stats.php 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852
  1. <?php
  2. /**
  3. * Module Name: Site Stats
  4. * Module Description: Collect valuable traffic stats and insights.
  5. * Sort Order: 1
  6. * Recommendation Order: 2
  7. * First Introduced: 1.1
  8. * Requires Connection: Yes
  9. * Auto Activate: Yes
  10. * Module Tags: Site Stats, Recommended
  11. * Feature: Engagement
  12. * Additional Search Queries: statistics, tracking, analytics, views, traffic, stats
  13. *
  14. * @package Jetpack
  15. */
  16. if ( defined( 'STATS_VERSION' ) ) {
  17. return;
  18. }
  19. define( 'STATS_VERSION', '9' );
  20. defined( 'STATS_DASHBOARD_SERVER' ) or define( 'STATS_DASHBOARD_SERVER', 'dashboard.wordpress.com' );
  21. add_action( 'jetpack_modules_loaded', 'stats_load' );
  22. /**
  23. * Load Stats.
  24. *
  25. * @access public
  26. * @return void
  27. */
  28. function stats_load() {
  29. Jetpack::enable_module_configurable( __FILE__ );
  30. Jetpack::module_configuration_load( __FILE__, 'stats_configuration_load' );
  31. Jetpack::module_configuration_head( __FILE__, 'stats_configuration_head' );
  32. Jetpack::module_configuration_screen( __FILE__, 'stats_configuration_screen' );
  33. // Generate the tracking code after wp() has queried for posts.
  34. add_action( 'template_redirect', 'stats_template_redirect', 1 );
  35. add_action( 'wp_head', 'stats_admin_bar_head', 100 );
  36. add_action( 'wp_head', 'stats_hide_smile_css' );
  37. add_action( 'jetpack_admin_menu', 'stats_admin_menu' );
  38. // Map stats caps.
  39. add_filter( 'map_meta_cap', 'stats_map_meta_caps', 10, 3 );
  40. if ( isset( $_GET['oldwidget'] ) ) {
  41. // Old one.
  42. add_action( 'wp_dashboard_setup', 'stats_register_dashboard_widget' );
  43. } else {
  44. add_action( 'admin_init', 'stats_merged_widget_admin_init' );
  45. }
  46. add_filter( 'jetpack_xmlrpc_methods', 'stats_xmlrpc_methods' );
  47. add_filter( 'pre_option_db_version', 'stats_ignore_db_version' );
  48. // Add an icon to see stats in WordPress.com for a particular post
  49. add_action( 'admin_print_styles-edit.php', 'jetpack_stats_load_admin_css' );
  50. add_filter( 'manage_posts_columns', 'jetpack_stats_post_table' );
  51. add_filter( 'manage_pages_columns', 'jetpack_stats_post_table' );
  52. add_action( 'manage_posts_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
  53. add_action( 'manage_pages_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
  54. }
  55. /**
  56. * Delay conditional for current_user_can to after init.
  57. *
  58. * @access public
  59. * @return void
  60. */
  61. function stats_merged_widget_admin_init() {
  62. if ( current_user_can( 'view_stats' ) ) {
  63. add_action( 'load-index.php', 'stats_enqueue_dashboard_head' );
  64. add_action( 'wp_dashboard_setup', 'stats_register_widget_control_callback' ); // Hacky but works.
  65. add_action( 'jetpack_dashboard_widget', 'stats_jetpack_dashboard_widget' );
  66. }
  67. }
  68. /**
  69. * Enqueue Stats Dashboard
  70. *
  71. * @access public
  72. * @return void
  73. */
  74. function stats_enqueue_dashboard_head() {
  75. add_action( 'admin_head', 'stats_dashboard_head' );
  76. }
  77. /**
  78. * Checks if filter is set and dnt is enabled.
  79. *
  80. * @return bool
  81. */
  82. function jetpack_is_dnt_enabled() {
  83. /**
  84. * Filter the option which decides honor DNT or not.
  85. *
  86. * @module stats
  87. * @since 6.1.0
  88. *
  89. * @param bool false Honors DNT for clients who don't want to be tracked. Defaults to false. Set to true to enable.
  90. */
  91. if ( false === apply_filters( 'jetpack_honor_dnt_header_for_stats', false ) ) {
  92. return false;
  93. }
  94. foreach ( $_SERVER as $name => $value ) {
  95. if ( 'http_dnt' == strtolower( $name ) && 1 == $value ) {
  96. return true;
  97. }
  98. }
  99. return false;
  100. }
  101. /**
  102. * Prevent sparkline img requests being redirected to upgrade.php.
  103. * See wp-admin/admin.php where it checks $wp_db_version.
  104. *
  105. * @access public
  106. * @param mixed $version Version.
  107. * @return string $version.
  108. */
  109. function stats_ignore_db_version( $version ) {
  110. if (
  111. is_admin() &&
  112. isset( $_GET['page'] ) && 'stats' === $_GET['page'] &&
  113. isset( $_GET['chart'] ) && strpos($_GET['chart'], 'admin-bar-hours') === 0
  114. ) {
  115. global $wp_db_version;
  116. return $wp_db_version;
  117. }
  118. return $version;
  119. }
  120. /**
  121. * Maps view_stats cap to read cap as needed.
  122. *
  123. * @access public
  124. * @param mixed $caps Caps.
  125. * @param mixed $cap Cap.
  126. * @param mixed $user_id User ID.
  127. * @return array Possibly mapped capabilities for meta capability.
  128. */
  129. function stats_map_meta_caps( $caps, $cap, $user_id ) {
  130. // Map view_stats to exists.
  131. if ( 'view_stats' === $cap ) {
  132. $user = new WP_User( $user_id );
  133. $user_role = array_shift( $user->roles );
  134. $stats_roles = stats_get_option( 'roles' );
  135. // Is the users role in the available stats roles?
  136. if ( is_array( $stats_roles ) && in_array( $user_role, $stats_roles ) ) {
  137. $caps = array( 'read' );
  138. }
  139. }
  140. return $caps;
  141. }
  142. /**
  143. * Stats Template Redirect.
  144. *
  145. * @access public
  146. * @return void
  147. */
  148. function stats_template_redirect() {
  149. global $current_user, $rendered_stats_footer;
  150. if ( is_feed() || is_robots() || is_trackback() || is_preview() || jetpack_is_dnt_enabled() ) {
  151. return;
  152. }
  153. // Should we be counting this user's views?
  154. if ( ! empty( $current_user->ID ) ) {
  155. $count_roles = stats_get_option( 'count_roles' );
  156. if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
  157. return;
  158. }
  159. }
  160. add_action( 'wp_footer', 'stats_footer', 101 );
  161. add_action( 'wp_head', 'stats_add_shutdown_action' );
  162. $rendered_stats_footer = false;
  163. }
  164. /**
  165. * Stats Build View Data.
  166. *
  167. * @access public
  168. * @return array.
  169. */
  170. function stats_build_view_data() {
  171. global $wp_the_query;
  172. $blog = Jetpack_Options::get_option( 'id' );
  173. $tz = get_option( 'gmt_offset' );
  174. $v = 'ext';
  175. $blog_url = parse_url( site_url() );
  176. $srv = $blog_url['host'];
  177. $j = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
  178. if ( $wp_the_query->is_single || $wp_the_query->is_page || $wp_the_query->is_posts_page ) {
  179. // Store and reset the queried_object and queried_object_id
  180. // Otherwise, redirect_canonical() will redirect to home_url( '/' ) for show_on_front = page sites where home_url() is not all lowercase.
  181. // Repro:
  182. // 1. Set home_url = https://ExamPle.com/
  183. // 2. Set show_on_front = page
  184. // 3. Set page_on_front = something
  185. // 4. Visit https://example.com/ !
  186. $queried_object = ( isset( $wp_the_query->queried_object ) ) ? $wp_the_query->queried_object : null;
  187. $queried_object_id = ( isset( $wp_the_query->queried_object_id ) ) ? $wp_the_query->queried_object_id : null;
  188. $post = $wp_the_query->get_queried_object_id();
  189. $wp_the_query->queried_object = $queried_object;
  190. $wp_the_query->queried_object_id = $queried_object_id;
  191. } else {
  192. $post = '0';
  193. }
  194. return compact( 'v', 'j', 'blog', 'post', 'tz', 'srv' );
  195. }
  196. /**
  197. * Stats Add Shutdown Action.
  198. *
  199. * @access public
  200. * @return void
  201. */
  202. function stats_add_shutdown_action() {
  203. // Just in case wp_footer isn't in your theme.
  204. add_action( 'shutdown', 'stats_footer', 101 );
  205. }
  206. /**
  207. * Stats Footer.
  208. *
  209. * @access public
  210. * @return void
  211. */
  212. function stats_footer() {
  213. global $rendered_stats_footer;
  214. if ( ! $rendered_stats_footer ) {
  215. $data = stats_build_view_data();
  216. if ( Jetpack_AMP_Support::is_amp_request() ) {
  217. stats_render_amp_footer( $data );
  218. } else {
  219. stats_render_footer( $data );
  220. }
  221. $rendered_stats_footer = true;
  222. }
  223. }
  224. function stats_render_footer( $data ) {
  225. $script = 'https://stats.wp.com/e-' . gmdate( 'YW' ) . '.js';
  226. $data_stats_array = stats_array( $data );
  227. $stats_footer = <<<END
  228. <script type='text/javascript' src='{$script}' async='async' defer='defer'></script>
  229. <script type='text/javascript'>
  230. _stq = window._stq || [];
  231. _stq.push([ 'view', {{$data_stats_array}} ]);
  232. _stq.push([ 'clickTrackerInit', '{$data['blog']}', '{$data['post']}' ]);
  233. </script>
  234. END;
  235. print $stats_footer;
  236. }
  237. function stats_render_amp_footer( $data ) {
  238. $data['host'] = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // input var ok.
  239. $data['rand'] = 'RANDOM'; // AMP placeholder.
  240. $data['ref'] = 'DOCUMENT_REFERRER'; // AMP placeholder.
  241. $data = array_map( 'rawurlencode', $data );
  242. $pixel_url = add_query_arg( $data, 'https://pixel.wp.com/g.gif' );
  243. ?>
  244. <amp-pixel src="<?php echo esc_url( $pixel_url ); ?>"></amp-pixel>
  245. <?php
  246. }
  247. /**
  248. * Stats Get Options.
  249. *
  250. * @access public
  251. * @return array.
  252. */
  253. function stats_get_options() {
  254. $options = get_option( 'stats_options' );
  255. if ( ! isset( $options['version'] ) || $options['version'] < STATS_VERSION ) {
  256. $options = stats_upgrade_options( $options );
  257. }
  258. return $options;
  259. }
  260. /**
  261. * Get Stats Options.
  262. *
  263. * @access public
  264. * @param mixed $option Option.
  265. * @return mixed|null.
  266. */
  267. function stats_get_option( $option ) {
  268. $options = stats_get_options();
  269. if ( 'blog_id' === $option ) {
  270. return Jetpack_Options::get_option( 'id' );
  271. }
  272. if ( isset( $options[ $option ] ) ) {
  273. return $options[ $option ];
  274. }
  275. return null;
  276. }
  277. /**
  278. * Stats Set Options.
  279. *
  280. * @access public
  281. * @param mixed $option Option.
  282. * @param mixed $value Value.
  283. * @return bool.
  284. */
  285. function stats_set_option( $option, $value ) {
  286. $options = stats_get_options();
  287. $options[ $option ] = $value;
  288. return stats_set_options( $options );
  289. }
  290. /**
  291. * Stats Set Options.
  292. *
  293. * @access public
  294. * @param mixed $options Options.
  295. * @return bool
  296. */
  297. function stats_set_options( $options ) {
  298. return update_option( 'stats_options', $options );
  299. }
  300. /**
  301. * Stats Upgrade Options.
  302. *
  303. * @access public
  304. * @param mixed $options Options.
  305. * @return array|bool
  306. */
  307. function stats_upgrade_options( $options ) {
  308. $defaults = array(
  309. 'admin_bar' => true,
  310. 'roles' => array( 'administrator' ),
  311. 'count_roles' => array(),
  312. 'blog_id' => Jetpack_Options::get_option( 'id' ),
  313. 'do_not_track' => true, // @todo
  314. 'hide_smile' => true,
  315. );
  316. if ( isset( $options['reg_users'] ) ) {
  317. if ( ! function_exists( 'get_editable_roles' ) ) {
  318. require_once ABSPATH . 'wp-admin/includes/user.php';
  319. }
  320. if ( $options['reg_users'] ) {
  321. $options['count_roles'] = array_keys( get_editable_roles() );
  322. }
  323. unset( $options['reg_users'] );
  324. }
  325. if ( is_array( $options ) && ! empty( $options ) ) {
  326. $new_options = array_merge( $defaults, $options );
  327. } else { $new_options = $defaults;
  328. }
  329. $new_options['version'] = STATS_VERSION;
  330. if ( ! stats_set_options( $new_options ) ) {
  331. return false;
  332. }
  333. stats_update_blog();
  334. return $new_options;
  335. }
  336. /**
  337. * Stats Array.
  338. *
  339. * @access public
  340. * @param mixed $kvs KVS.
  341. * @return array
  342. */
  343. function stats_array( $kvs ) {
  344. /**
  345. * Filter the options added to the JavaScript Stats tracking code.
  346. *
  347. * @module stats
  348. *
  349. * @since 1.1.0
  350. *
  351. * @param array $kvs Array of options about the site and page you're on.
  352. */
  353. $kvs = apply_filters( 'stats_array', $kvs );
  354. $kvs = array_map( 'addslashes', $kvs );
  355. foreach ( $kvs as $k => $v ) {
  356. $jskvs[] = "$k:'$v'";
  357. }
  358. return join( ',', $jskvs );
  359. }
  360. /**
  361. * Admin Pages.
  362. *
  363. * @access public
  364. * @return void
  365. */
  366. function stats_admin_menu() {
  367. global $pagenow;
  368. // If we're at an old Stats URL, redirect to the new one.
  369. // Don't even bother with caps, menu_page_url(), etc. Just do it.
  370. if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page'] ) {
  371. $redirect_url = str_replace( array( '/wp-admin/index.php?', '/wp-admin/?' ), '/wp-admin/admin.php?', $_SERVER['REQUEST_URI'] );
  372. $relative_pos = strpos( $redirect_url, '/wp-admin/' );
  373. if ( false !== $relative_pos ) {
  374. wp_safe_redirect( admin_url( substr( $redirect_url, $relative_pos + 10 ) ) );
  375. exit;
  376. }
  377. }
  378. $hook = add_submenu_page( 'jetpack', __( 'Site Stats', 'jetpack' ), __( 'Site Stats', 'jetpack' ), 'view_stats', 'stats', 'stats_reports_page' );
  379. add_action( "load-$hook", 'stats_reports_load' );
  380. }
  381. /**
  382. * Stats Admin Path.
  383. *
  384. * @access public
  385. * @return string
  386. */
  387. function stats_admin_path() {
  388. return Jetpack::module_configuration_url( __FILE__ );
  389. }
  390. /**
  391. * Stats Reports Load.
  392. *
  393. * @access public
  394. * @return void
  395. */
  396. function stats_reports_load() {
  397. wp_enqueue_script( 'jquery' );
  398. wp_enqueue_script( 'postbox' );
  399. wp_enqueue_script( 'underscore' );
  400. $rtl = is_rtl() ? '.rtl' : '';
  401. wp_enqueue_style( 'dops-css', plugins_url( "_inc/build/admin.dops-style$rtl.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
  402. wp_enqueue_style( 'components-css', plugins_url( "_inc/build/style.min$rtl.css", JETPACK__PLUGIN_FILE ), array(), JETPACK__VERSION );
  403. add_action( 'admin_print_styles', 'stats_reports_css' );
  404. if ( isset( $_GET['nojs'] ) && $_GET['nojs'] ) {
  405. $parsed = parse_url( admin_url() );
  406. // Remember user doesn't want JS.
  407. setcookie( 'stnojs', '1', time() + 172800, $parsed['path'] ); // 2 days.
  408. }
  409. if ( isset( $_COOKIE['stnojs'] ) && $_COOKIE['stnojs'] ) {
  410. // Detect if JS is on. If so, remove cookie so next page load is via JS.
  411. add_action( 'admin_print_footer_scripts', 'stats_js_remove_stnojs_cookie' );
  412. } else if ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) ) {
  413. // Normal page load. Load page content via JS.
  414. add_action( 'admin_print_footer_scripts', 'stats_js_load_page_via_ajax' );
  415. }
  416. }
  417. /**
  418. * Stats Reports CSS.
  419. *
  420. * @access public
  421. * @return void
  422. */
  423. function stats_reports_css() {
  424. ?>
  425. <style type="text/css">
  426. #wpcontent {
  427. padding-left: 0 !important;
  428. }
  429. .jetpack_page_stats {
  430. background-color: #f3f6f8;
  431. }
  432. #jp-stats-wrap {
  433. max-width: 1040px;
  434. margin: 0 auto;
  435. overflow: hidden;
  436. }
  437. #stats-loading-wrap p {
  438. text-align: center;
  439. font-size: 2em;
  440. margin: 7.5em 15px 0 0;
  441. height: 64px;
  442. line-height: 64px;
  443. }
  444. </style>
  445. <?php
  446. }
  447. /**
  448. * Detect if JS is on. If so, remove cookie so next page load is via JS.
  449. *
  450. * @access public
  451. * @return void
  452. */
  453. function stats_js_remove_stnojs_cookie() {
  454. $parsed = parse_url( admin_url() );
  455. ?>
  456. <script type="text/javascript">
  457. /* <![CDATA[ */
  458. document.cookie = 'stnojs=0; expires=Wed, 9 Mar 2011 16:55:50 UTC; path=<?php echo esc_js( $parsed['path'] ); ?>';
  459. /* ]]> */
  460. </script>
  461. <?php
  462. }
  463. /**
  464. * Normal page load. Load page content via JS.
  465. *
  466. * @access public
  467. * @return void
  468. */
  469. function stats_js_load_page_via_ajax() {
  470. ?>
  471. <script type="text/javascript">
  472. /* <![CDATA[ */
  473. if ( -1 == document.location.href.indexOf( 'noheader' ) ) {
  474. jQuery( function( $ ) {
  475. $.get( document.location.href + '&noheader', function( responseText ) {
  476. $( '#stats-loading-wrap' ).replaceWith( responseText );
  477. } );
  478. } );
  479. }
  480. /* ]]> */
  481. </script>
  482. <?php
  483. }
  484. /**
  485. * Stats Report Page.
  486. *
  487. * @access public
  488. * @param bool $main_chart_only (default: false) Main Chart Only.
  489. */
  490. function stats_reports_page( $main_chart_only = false ) {
  491. if ( isset( $_GET['dashboard'] ) ) {
  492. return stats_dashboard_widget_content();
  493. }
  494. $blog_id = stats_get_option( 'blog_id' );
  495. $domain = Jetpack::build_raw_urls( get_home_url() );
  496. $jetpack_admin_url = admin_url() . 'admin.php?page=jetpack';
  497. if ( ! $main_chart_only && ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) {
  498. $nojs_url = add_query_arg( 'nojs', '1' );
  499. $http = is_ssl() ? 'https' : 'http';
  500. // Loading message. No JS fallback message.
  501. ?>
  502. <div id="jp-plugin-container">
  503. <div class="jp-masthead">
  504. <div class="jp-masthead__inside-container">
  505. <div class="jp-masthead__logo-container">
  506. <a class="jp-masthead__logo-link" href="<?php echo esc_url( $jetpack_admin_url ); ?>">
  507. <svg class="jetpack-logo__masthead" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" height="32" viewBox="0 0 118 32"><path fill="#00BE28" d="M16,0C7.2,0,0,7.2,0,16s7.2,16,16,16s16-7.2,16-16S24.8,0,16,0z M15,19H7l8-16V19z M17,29V13h8L17,29z"></path><path d="M41.3,26.6c-0.5-0.7-0.9-1.4-1.3-2.1c2.3-1.4,3-2.5,3-4.6V8h-3V6h6v13.4C46,22.8,45,24.8,41.3,26.6z"></path><path d="M65,18.4c0,1.1,0.8,1.3,1.4,1.3c0.5,0,2-0.2,2.6-0.4v2.1c-0.9,0.3-2.5,0.5-3.7,0.5c-1.5,0-3.2-0.5-3.2-3.1V12H60v-2h2.1V7.1 H65V10h4v2h-4V18.4z"></path><path d="M71,10h3v1.3c1.1-0.8,1.9-1.3,3.3-1.3c2.5,0,4.5,1.8,4.5,5.6s-2.2,6.3-5.8,6.3c-0.9,0-1.3-0.1-2-0.3V28h-3V10z M76.5,12.3 c-0.8,0-1.6,0.4-2.5,1.2v5.9c0.6,0.1,0.9,0.2,1.8,0.2c2,0,3.2-1.3,3.2-3.9C79,13.4,78.1,12.3,76.5,12.3z"></path><path d="M93,22h-3v-1.5c-0.9,0.7-1.9,1.5-3.5,1.5c-1.5,0-3.1-1.1-3.1-3.2c0-2.9,2.5-3.4,4.2-3.7l2.4-0.3v-0.3c0-1.5-0.5-2.3-2-2.3 c-0.7,0-2.3,0.5-3.7,1.1L84,11c1.2-0.4,3-1,4.4-1c2.7,0,4.6,1.4,4.6,4.7L93,22z M90,16.4l-2.2,0.4c-0.7,0.1-1.4,0.5-1.4,1.6 c0,0.9,0.5,1.4,1.3,1.4s1.5-0.5,2.3-1V16.4z"></path><path d="M104.5,21.3c-1.1,0.4-2.2,0.6-3.5,0.6c-4.2,0-5.9-2.4-5.9-5.9c0-3.7,2.3-6,6.1-6c1.4,0,2.3,0.2,3.2,0.5V13 c-0.8-0.3-2-0.6-3.2-0.6c-1.7,0-3.2,0.9-3.2,3.6c0,2.9,1.5,3.8,3.3,3.8c0.9,0,1.9-0.2,3.2-0.7V21.3z"></path><path d="M110,15.2c0.2-0.3,0.2-0.8,3.8-5.2h3.7l-4.6,5.7l5,6.3h-3.7l-4.2-5.8V22h-3V6h3V15.2z"></path><path d="M58.5,21.3c-1.5,0.5-2.7,0.6-4.2,0.6c-3.6,0-5.8-1.8-5.8-6c0-3.1,1.9-5.9,5.5-5.9s4.9,2.5,4.9,4.9c0,0.8,0,1.5-0.1,2h-7.3 c0.1,2.5,1.5,2.8,3.6,2.8c1.1,0,2.2-0.3,3.4-0.7C58.5,19,58.5,21.3,58.5,21.3z M56,15c0-1.4-0.5-2.9-2-2.9c-1.4,0-2.3,1.3-2.4,2.9 C51.6,15,56,15,56,15z"></path></svg>
  508. </a>
  509. </div>
  510. <div class="jp-masthead__nav"><span class="dops-button-group"><a href="<?php echo esc_url( $jetpack_admin_url ); ?>" type="button" class="dops-button is-compact"><?php esc_html_e( 'Dashboard', 'jetpack' ); ?></a><a href="<?php echo esc_url( $jetpack_admin_url . '#/settings' ); ?>" type="button" class="dops-button is-compact"><?php esc_html_e( 'Settings', 'jetpack' ); ?></a></span></div>
  511. </div>
  512. </div>
  513. <div id="jp-stats-wrap">
  514. <div class="wrap">
  515. <h2><?php esc_html_e( 'Site Stats', 'jetpack' ); ?> <?php if ( current_user_can( 'jetpack_manage_modules' ) ) : ?><a style="font-size:13px;" href="<?php echo esc_url( admin_url( 'admin.php?page=jetpack&configure=stats' ) ); ?>"><?php esc_html_e( 'Configure', 'jetpack' ); ?></a><?php endif; ?></h2>
  516. </div>
  517. <div id="stats-loading-wrap" class="wrap">
  518. <p class="hide-if-no-js"><img width="32" height="32" alt="<?php esc_attr_e( 'Loading&hellip;', 'jetpack' ); ?>" src="<?php
  519. echo esc_url(
  520. /**
  521. * Sets external resource URL.
  522. *
  523. * @module stats
  524. *
  525. * @since 1.4.0
  526. *
  527. * @param string $args URL of external resource.
  528. */
  529. apply_filters( 'jetpack_static_url', "{$http}://en.wordpress.com/i/loading/loading-64.gif" )
  530. ); ?>" /></p>
  531. <p style="font-size: 11pt; margin: 0;"><a href="https://wordpress.com/stats/<?php echo esc_attr( $domain ); ?>" target="_blank"><?php esc_html_e( 'View stats on WordPress.com right now', 'jetpack' ); ?></a></p>
  532. <p class="hide-if-js"><?php esc_html_e( 'Your Site Stats work better with JavaScript enabled.', 'jetpack' ); ?><br />
  533. <a href="<?php echo esc_url( $nojs_url ); ?>"><?php esc_html_e( 'View Site Stats without JavaScript', 'jetpack' ); ?></a>.</p>
  534. </div>
  535. </div>
  536. <div class="jp-footer">
  537. <ul class="jp-footer__links">
  538. <li class="jp-footer__link-item">
  539. <a href="https://jetpack.com" target="_blank" rel="noopener noreferrer" class="jp-footer__link" title="<?php esc_html_e( 'Jetpack version', 'jetpack' ); ?>">Jetpack <?php echo JETPACK__VERSION; ?></a>
  540. </li>
  541. <li class="jp-footer__link-item">
  542. <a href="https://wordpress.com/tos/" target="_blank" rel="noopener noreferrer" title="<?php esc_html__( 'WordPress.com Terms of Service', 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Terms', 'Navigation item', 'jetpack' ); ?></a>
  543. </li>
  544. <li class="jp-footer__link-item">
  545. <a href="<?php echo esc_url( $jetpack_admin_url . '#/privacy' ); ?>" rel="noopener noreferrer" title="<?php esc_html_e( 'Automattic\'s Privacy Policy', 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Privacy', 'Navigation item', 'jetpack' ); ?></a>
  546. </li>
  547. <li class="jp-footer__link-item">
  548. <a href="<?php echo esc_url( admin_url() . 'admin.php?page=jetpack-debugger' ); ?>" title="<?php esc_html_e( 'Test your site\'s compatibility with Jetpack.', 'jetpack' ); ?>" class="jp-footer__link"><?php echo esc_html_x( 'Debug', 'Navigation item', 'jetpack' ); ?></a>
  549. </li>
  550. </ul>
  551. </div>
  552. </div>
  553. <?php
  554. return;
  555. }
  556. $day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false;
  557. $q = array(
  558. 'noheader' => 'true',
  559. 'proxy' => '',
  560. 'page' => 'stats',
  561. 'day' => $day,
  562. 'blog' => $blog_id,
  563. 'charset' => get_option( 'blog_charset' ),
  564. 'color' => get_user_option( 'admin_color' ),
  565. 'ssl' => is_ssl(),
  566. 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
  567. );
  568. if ( get_locale() !== 'en_US' ) {
  569. $q['jp_lang'] = get_locale();
  570. }
  571. // Only show the main chart, without extra header data, or metaboxes.
  572. $q['main_chart_only'] = $main_chart_only;
  573. $args = array(
  574. 'view' => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
  575. 'numdays' => 'int',
  576. 'day' => 'date',
  577. 'unit' => array( 1, 7, 31, 'human' ),
  578. 'humanize' => array( 'true' ),
  579. 'num' => 'int',
  580. 'summarize' => null,
  581. 'post' => 'int',
  582. 'width' => 'int',
  583. 'height' => 'int',
  584. 'data' => 'data',
  585. 'blog_subscribers' => 'int',
  586. 'comment_subscribers' => null,
  587. 'type' => array( 'wpcom', 'email', 'pending' ),
  588. 'pagenum' => 'int',
  589. );
  590. foreach ( $args as $var => $vals ) {
  591. if ( ! isset( $_REQUEST[$var] ) )
  592. continue;
  593. if ( is_array( $vals ) ) {
  594. if ( in_array( $_REQUEST[$var], $vals ) )
  595. $q[$var] = $_REQUEST[$var];
  596. } elseif ( 'int' === $vals ) {
  597. $q[$var] = intval( $_REQUEST[$var] );
  598. } elseif ( 'date' === $vals ) {
  599. if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_REQUEST[$var] ) )
  600. $q[$var] = $_REQUEST[$var];
  601. } elseif ( null === $vals ) {
  602. $q[$var] = '';
  603. } elseif ( 'data' === $vals ) {
  604. if ( 'index.php' === substr( $_REQUEST[$var], 0, 9 ) )
  605. $q[$var] = $_REQUEST[$var];
  606. }
  607. }
  608. if ( isset( $_GET['chart'] ) ) {
  609. if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) {
  610. $chart = sanitize_title( $_GET['chart'] );
  611. $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.php";
  612. }
  613. } else {
  614. $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-admin/index.php";
  615. }
  616. $url = add_query_arg( $q, $url );
  617. $method = 'GET';
  618. $timeout = 90;
  619. $user_id = JETPACK_MASTER_USER; // means send the wp.com user_id
  620. $get = Jetpack_Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
  621. $get_code = wp_remote_retrieve_response_code( $get );
  622. if ( is_wp_error( $get ) || ( 2 !== intval( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
  623. stats_print_wp_remote_error( $get, $url );
  624. } else {
  625. if ( ! empty( $get['headers']['content-type'] ) ) {
  626. $type = $get['headers']['content-type'];
  627. if ( substr( $type, 0, 5 ) === 'image' ) {
  628. $img = $get['body'];
  629. header( 'Content-Type: ' . $type );
  630. header( 'Content-Length: ' . strlen( $img ) );
  631. echo $img;
  632. die();
  633. }
  634. }
  635. $body = stats_convert_post_titles( $get['body'] );
  636. $body = stats_convert_chart_urls( $body );
  637. $body = stats_convert_image_urls( $body );
  638. $body = stats_convert_admin_urls( $body );
  639. echo $body;
  640. }
  641. if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) {
  642. JetpackTracking::record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
  643. }
  644. if ( isset( $_GET['noheader'] ) ) {
  645. die;
  646. }
  647. }
  648. /**
  649. * Stats Convert Admin Urls.
  650. *
  651. * @access public
  652. * @param mixed $html HTML.
  653. * @return string
  654. */
  655. function stats_convert_admin_urls( $html ) {
  656. return str_replace( 'index.php?page=stats', 'admin.php?page=stats', $html );
  657. }
  658. /**
  659. * Stats Convert Image URLs.
  660. *
  661. * @access public
  662. * @param mixed $html HTML.
  663. * @return string
  664. */
  665. function stats_convert_image_urls( $html ) {
  666. $url = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
  667. $html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
  668. return $html;
  669. }
  670. /**
  671. * Callback for preg_replace_callback used in stats_convert_chart_urls()
  672. *
  673. * @since 5.6.0
  674. *
  675. * @param array $matches The matches resulting from the preg_replace_callback call.
  676. * @return string The admin url for the chart.
  677. */
  678. function jetpack_stats_convert_chart_urls_callback( $matches ) {
  679. // If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
  680. return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
  681. }
  682. /**
  683. * Stats Convert Chart URLs.
  684. *
  685. * @access public
  686. * @param mixed $html HTML.
  687. * @return string
  688. */
  689. function stats_convert_chart_urls( $html ) {
  690. $html = preg_replace_callback(
  691. '|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
  692. 'jetpack_stats_convert_chart_urls_callback',
  693. $html
  694. );
  695. return $html;
  696. }
  697. /**
  698. * Stats Convert Post Title HTML
  699. *
  700. * @access public
  701. * @param mixed $html HTML.
  702. * @return string
  703. */
  704. function stats_convert_post_titles( $html ) {
  705. global $stats_posts;
  706. $pattern = "<span class='post-(\d+)-link'>.*?</span>";
  707. if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
  708. return $html;
  709. }
  710. $posts = get_posts(
  711. array(
  712. 'include' => implode( ',', $matches[1] ),
  713. 'post_type' => 'any',
  714. 'post_status' => 'any',
  715. 'numberposts' => -1,
  716. 'suppress_filters' => false,
  717. )
  718. );
  719. foreach ( $posts as $post ) {
  720. $stats_posts[ $post->ID ] = $post;
  721. }
  722. $html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
  723. return $html;
  724. }
  725. /**
  726. * Stats Convert Post Title Matches.
  727. *
  728. * @access public
  729. * @param mixed $matches Matches.
  730. * @return string
  731. */
  732. function stats_convert_post_title( $matches ) {
  733. global $stats_posts;
  734. $post_id = $matches[1];
  735. if ( isset( $stats_posts[$post_id] ) )
  736. return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
  737. return $matches[0];
  738. }
  739. /**
  740. * Stats Configuration Load.
  741. *
  742. * @access public
  743. * @return void
  744. */
  745. function stats_configuration_load() {
  746. if ( isset( $_POST['action'] ) && 'save_options' === $_POST['action'] && $_POST['_wpnonce'] === wp_create_nonce( 'stats' ) ) {
  747. $options = stats_get_options();
  748. $options['admin_bar'] = isset( $_POST['admin_bar'] ) && $_POST['admin_bar'];
  749. $options['hide_smile'] = isset( $_POST['hide_smile'] ) && $_POST['hide_smile'];
  750. $options['roles'] = array( 'administrator' );
  751. foreach ( get_editable_roles() as $role => $details ) {
  752. if ( isset( $_POST["role_$role"] ) && $_POST["role_$role"] ) {
  753. $options['roles'][] = $role;
  754. }
  755. }
  756. $options['count_roles'] = array();
  757. foreach ( get_editable_roles() as $role => $details ) {
  758. if ( isset( $_POST["count_role_$role"] ) && $_POST["count_role_$role"] ) {
  759. $options['count_roles'][] = $role;
  760. }
  761. }
  762. stats_set_options( $options );
  763. stats_update_blog();
  764. Jetpack::state( 'message', 'module_configured' );
  765. wp_safe_redirect( Jetpack::module_configuration_url( 'stats' ) );
  766. exit;
  767. }
  768. }
  769. /**
  770. * Stats Configuration Head.
  771. *
  772. * @access public
  773. * @return void
  774. */
  775. function stats_configuration_head() {
  776. ?>
  777. <style type="text/css">
  778. #statserror {
  779. border: 1px solid #766;
  780. background-color: #d22;
  781. padding: 1em 3em;
  782. }
  783. .stats-smiley {
  784. vertical-align: 1px;
  785. }
  786. </style>
  787. <?php
  788. }
  789. /**
  790. * Stats Configuration Screen.
  791. *
  792. * @access public
  793. * @return void
  794. */
  795. function stats_configuration_screen() {
  796. $options = stats_get_options();
  797. ?>
  798. <div class="narrow">
  799. <p><?php printf( __( 'Visit <a href="%s">Site Stats</a> to see your stats.', 'jetpack' ), esc_url( menu_page_url( 'stats', false ) ) ); ?></p>
  800. <form method="post">
  801. <input type='hidden' name='action' value='save_options' />
  802. <?php wp_nonce_field( 'stats' ); ?>
  803. <table id="menu" class="form-table">
  804. <tr valign="top"><th scope="row"><label for="admin_bar"><?php esc_html_e( 'Admin bar' , 'jetpack' ); ?></label></th>
  805. <td><label><input type='checkbox'<?php checked( $options['admin_bar'] ); ?> name='admin_bar' id='admin_bar' /> <?php esc_html_e( 'Put a chart showing 48 hours of views in the admin bar.', 'jetpack' ); ?></label></td></tr>
  806. <tr valign="top"><th scope="row"><?php esc_html_e( 'Registered users', 'jetpack' ); ?></th>
  807. <td>
  808. <?php esc_html_e( "Count the page views of registered users who are logged in.", 'jetpack' ); ?><br/>
  809. <?php
  810. $count_roles = stats_get_option( 'count_roles' );
  811. foreach ( get_editable_roles() as $role => $details ) {
  812. ?>
  813. <label><input type='checkbox' name='count_role_<?php echo $role; ?>'<?php checked( in_array( $role, $count_roles ) ); ?> /> <?php echo translate_user_role( $details['name'] ); ?></label><br/>
  814. <?php
  815. }
  816. ?>
  817. </td></tr>
  818. <tr valign="top"><th scope="row"><?php esc_html_e( 'Smiley' , 'jetpack' ); ?></th>
  819. <td><label><input type='checkbox'<?php checked( isset( $options['hide_smile'] ) && $options['hide_smile'] ); ?> name='hide_smile' id='hide_smile' /> <?php esc_html_e( 'Hide the stats smiley face image.', 'jetpack' ); ?></label><br /> <span class="description"><?php echo wp_kses( __( 'The image helps collect stats and <strong>makes the world a better place</strong> but should still work when hidden', 'jetpack' ), array( 'strong' => array() ) ); ?> <img class="stats-smiley" alt="<?php esc_attr_e( 'Smiley face', 'jetpack' ); ?>" src="<?php echo esc_url( plugins_url( 'images/stats-smiley.gif', dirname( __FILE__ ) ) ); ?>" width="6" height="5" /></span></td></tr>
  820. <tr valign="top"><th scope="row"><?php esc_html_e( 'Report visibility' , 'jetpack' ); ?></th>
  821. <td>
  822. <?php esc_html_e( 'Select the roles that will be able to view stats reports.', 'jetpack' ); ?><br/>
  823. <?php
  824. $stats_roles = stats_get_option( 'roles' );
  825. foreach ( get_editable_roles() as $role => $details ) {
  826. ?>
  827. <label><input type='checkbox' <?php if ( 'administrator' === $role ) echo "disabled='disabled' "; ?>name='role_<?php echo $role; ?>'<?php checked( 'administrator' === $role || in_array( $role, $stats_roles ) ); ?> /> <?php echo translate_user_role( $details['name'] ); ?></label><br/>
  828. <?php
  829. }
  830. ?>
  831. </td></tr>
  832. </table>
  833. <p class="submit"><input type='submit' class='button-primary' value='<?php echo esc_attr( __( 'Save configuration', 'jetpack' ) ); ?>' /></p>
  834. </form>
  835. </div>
  836. <?php
  837. }
  838. /**
  839. * Stats Hide Smile.
  840. *
  841. * @access public
  842. * @return void
  843. */
  844. function stats_hide_smile_css() {
  845. $options = stats_get_options();
  846. if ( isset( $options['hide_smile'] ) && $options['hide_smile'] ) {
  847. ?>
  848. <style type='text/css'>img#wpstats{display:none}</style><?php
  849. }
  850. }
  851. /**
  852. * Stats Admin Bar Head.
  853. *
  854. * @access public
  855. * @return void
  856. */
  857. function stats_admin_bar_head() {
  858. if ( ! stats_get_option( 'admin_bar' ) )
  859. return;
  860. if ( ! current_user_can( 'view_stats' ) )
  861. return;
  862. if ( function_exists( 'is_admin_bar_showing' ) && ! is_admin_bar_showing() ) {
  863. return;
  864. }
  865. add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
  866. ?>
  867. <style type='text/css'>
  868. #wpadminbar .quicklinks li#wp-admin-bar-stats {
  869. height: 32px;
  870. }
  871. #wpadminbar .quicklinks li#wp-admin-bar-stats a {
  872. height: 32px;
  873. padding: 0;
  874. }
  875. #wpadminbar .quicklinks li#wp-admin-bar-stats a div {
  876. height: 32px;
  877. width: 95px;
  878. overflow: hidden;
  879. margin: 0 10px;
  880. }
  881. #wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
  882. width: auto;
  883. margin: 0 8px 0 10px;
  884. }
  885. #wpadminbar .quicklinks li#wp-admin-bar-stats a img {
  886. height: 24px;
  887. padding: 4px 0;
  888. max-width: none;
  889. border: none;
  890. }
  891. </style>
  892. <?php
  893. }
  894. /**
  895. * Stats AdminBar.
  896. *
  897. * @access public
  898. * @param mixed $wp_admin_bar WPAdminBar.
  899. * @return void
  900. */
  901. function stats_admin_bar_menu( &$wp_admin_bar ) {
  902. $url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ); // no menu_page_url() blog-side.
  903. $img_src = esc_attr( add_query_arg( array( 'noheader' => '', 'proxy' => '', 'chart' => 'admin-bar-hours-scale' ), $url ) );
  904. $img_src_2x = esc_attr( add_query_arg( array( 'noheader' => '', 'proxy' => '', 'chart' => 'admin-bar-hours-scale-2x' ), $url ) );
  905. $alt = esc_attr( __( 'Stats', 'jetpack' ) );
  906. $title = esc_attr( __( 'Views over 48 hours. Click for more Site Stats.', 'jetpack' ) );
  907. $menu = array( 'id' => 'stats', 'title' => "<div><script type='text/javascript'>var src;if(typeof(window.devicePixelRatio)=='undefined'||window.devicePixelRatio<2){src='$img_src';}else{src='$img_src_2x';}document.write('<img src=\''+src+'\' alt=\'$alt\' title=\'$title\' />');</script></div>", 'href' => $url );
  908. $wp_admin_bar->add_menu( $menu );
  909. }
  910. /**
  911. * Stats Update Blog.
  912. *
  913. * @access public
  914. * @return void
  915. */
  916. function stats_update_blog() {
  917. Jetpack::xmlrpc_async_call( 'jetpack.updateBlog', stats_get_blog() );
  918. }
  919. /**
  920. * Stats Get Blog.
  921. *
  922. * @access public
  923. * @return string
  924. */
  925. function stats_get_blog() {
  926. $home = parse_url( trailingslashit( get_option( 'home' ) ) );
  927. $blog = array(
  928. 'host' => $home['host'],
  929. 'path' => $home['path'],
  930. 'blogname' => get_option( 'blogname' ),
  931. 'blogdescription' => get_option( 'blogdescription' ),
  932. 'siteurl' => get_option( 'siteurl' ),
  933. 'gmt_offset' => get_option( 'gmt_offset' ),
  934. 'timezone_string' => get_option( 'timezone_string' ),
  935. 'stats_version' => STATS_VERSION,
  936. 'stats_api' => 'jetpack',
  937. 'page_on_front' => get_option( 'page_on_front' ),
  938. 'permalink_structure' => get_option( 'permalink_structure' ),
  939. 'category_base' => get_option( 'category_base' ),
  940. 'tag_base' => get_option( 'tag_base' ),
  941. );
  942. $blog = array_merge( stats_get_options(), $blog );
  943. unset( $blog['roles'], $blog['blog_id'] );
  944. return stats_esc_html_deep( $blog );
  945. }
  946. /**
  947. * Modified from stripslashes_deep()
  948. *
  949. * @access public
  950. * @param mixed $value Value.
  951. * @return string
  952. */
  953. function stats_esc_html_deep( $value ) {
  954. if ( is_array( $value ) ) {
  955. $value = array_map( 'stats_esc_html_deep', $value );
  956. } elseif ( is_object( $value ) ) {
  957. $vars = get_object_vars( $value );
  958. foreach ( $vars as $key => $data ) {
  959. $value->{$key} = stats_esc_html_deep( $data );
  960. }
  961. } elseif ( is_string( $value ) ) {
  962. $value = esc_html( $value );
  963. }
  964. return $value;
  965. }
  966. /**
  967. * Stats xmlrpc_methods function.
  968. *
  969. * @access public
  970. * @param mixed $methods Methods.
  971. * @return array
  972. */
  973. function stats_xmlrpc_methods( $methods ) {
  974. $my_methods = array(
  975. 'jetpack.getBlog' => 'stats_get_blog',
  976. );
  977. return array_merge( $methods, $my_methods );
  978. }
  979. /**
  980. * Register Stats Dashboard Widget.
  981. *
  982. * @access public
  983. * @return void
  984. */
  985. function stats_register_dashboard_widget() {
  986. if ( ! current_user_can( 'view_stats' ) )
  987. return;
  988. // With wp_dashboard_empty: we load in the content after the page load via JS.
  989. wp_add_dashboard_widget( 'dashboard_stats', __( 'Site Stats', 'jetpack' ), 'wp_dashboard_empty', 'stats_dashboard_widget_control' );
  990. add_action( 'admin_head', 'stats_dashboard_head' );
  991. }
  992. /**
  993. * Stats Dashboard Widget Options.
  994. *
  995. * @access public
  996. * @return array
  997. */
  998. function stats_dashboard_widget_options() {
  999. $defaults = array( 'chart' => 1, 'top' => 1, 'search' => 7 );
  1000. if ( ( ! $options = get_option( 'stats_dashboard_widget' ) ) || ! is_array( $options ) ) {
  1001. $options = array();
  1002. }
  1003. // Ignore obsolete option values.
  1004. $intervals = array( 1, 7, 31, 90, 365 );
  1005. foreach ( array( 'top', 'search' ) as $key ) {
  1006. if ( isset( $options[ $key ] ) && ! in_array( $options[ $key ], $intervals ) ) {
  1007. unset( $options[ $key ] );
  1008. }
  1009. }
  1010. return array_merge( $defaults, $options );
  1011. }
  1012. /**
  1013. * Stats Dashboard Widget Control.
  1014. *
  1015. * @access public
  1016. * @return void
  1017. */
  1018. function stats_dashboard_widget_control() {
  1019. $periods = array(
  1020. '1' => __( 'day', 'jetpack' ),
  1021. '7' => __( 'week', 'jetpack' ),
  1022. '31' => __( 'month', 'jetpack' ),
  1023. );
  1024. $intervals = array(
  1025. '1' => __( 'the past day', 'jetpack' ),
  1026. '7' => __( 'the past week', 'jetpack' ),
  1027. '31' => __( 'the past month', 'jetpack' ),
  1028. '90' => __( 'the past quarter', 'jetpack' ),
  1029. '365' => __( 'the past year', 'jetpack' ),
  1030. );
  1031. $defaults = array(
  1032. 'top' => 1,
  1033. 'search' => 7,
  1034. );
  1035. $options = stats_dashboard_widget_options();
  1036. if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) && isset( $_POST['widget_id'] ) && 'dashboard_stats' === $_POST['widget_id'] ) {
  1037. if ( isset( $periods[ $_POST['chart'] ] ) ) {
  1038. $options['chart'] = $_POST['chart'];
  1039. }
  1040. foreach ( array( 'top', 'search' ) as $key ) {
  1041. if ( isset( $intervals[ $_POST[ $key ] ] ) ) {
  1042. $options[ $key ] = $_POST[ $key ];
  1043. } else { $options[ $key ] = $defaults[ $key ];
  1044. }
  1045. }
  1046. update_option( 'stats_dashboard_widget', $options );
  1047. }
  1048. ?>
  1049. <p>
  1050. <label for="chart"><?php esc_html_e( 'Chart stats by' , 'jetpack' ); ?></label>
  1051. <select id="chart" name="chart">
  1052. <?php
  1053. foreach ( $periods as $val => $label ) {
  1054. ?>
  1055. <option value="<?php echo $val; ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
  1056. <?php
  1057. }
  1058. ?>
  1059. </select>.
  1060. </p>
  1061. <p>
  1062. <label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
  1063. <select id="top" name="top">
  1064. <?php
  1065. foreach ( $intervals as $val => $label ) {
  1066. ?>
  1067. <option value="<?php echo $val; ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
  1068. <?php
  1069. }
  1070. ?>
  1071. </select>.
  1072. </p>
  1073. <p>
  1074. <label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
  1075. <select id="search" name="search">
  1076. <?php
  1077. foreach ( $intervals as $val => $label ) {
  1078. ?>
  1079. <option value="<?php echo $val; ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
  1080. <?php
  1081. }
  1082. ?>
  1083. </select>.
  1084. </p>
  1085. <?php
  1086. }
  1087. /**
  1088. * Jetpack Stats Dashboard Widget.
  1089. *
  1090. * @access public
  1091. * @return void
  1092. */
  1093. function stats_jetpack_dashboard_widget() {
  1094. ?>
  1095. <form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
  1096. <?php stats_dashboard_widget_control(); ?>
  1097. <?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
  1098. <input type="hidden" name="widget_id" value="dashboard_stats" />
  1099. <?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
  1100. </form>
  1101. <span id="js-toggle-stats_dashboard_widget_control">
  1102. <?php esc_html_e( 'Configure', 'jetpack' ); ?>
  1103. </span>
  1104. <div id="dashboard_stats">
  1105. <div class="inside">
  1106. <div style="height: 250px;"></div>
  1107. </div>
  1108. </div>
  1109. <script>
  1110. jQuery(document).ready(function($){
  1111. var $toggle = $('#js-toggle-stats_dashboard_widget_control');
  1112. $toggle.parent().prev().append( $toggle );
  1113. $toggle.show().click(function(e){
  1114. e.preventDefault();
  1115. e.stopImmediatePropagation();
  1116. $(this).parent().toggleClass('controlVisible');
  1117. $('#stats_dashboard_widget_control').slideToggle();
  1118. });
  1119. });
  1120. </script>
  1121. <style>
  1122. #js-toggle-stats_dashboard_widget_control {
  1123. display: none;
  1124. float: right;
  1125. margin-top: 0.2em;
  1126. font-weight: 400;
  1127. color: #444;
  1128. font-size: .8em;
  1129. text-decoration: underline;
  1130. cursor: pointer;
  1131. }
  1132. #stats_dashboard_widget_control {
  1133. display: none;
  1134. padding: 0 10px;
  1135. overflow: hidden;
  1136. }
  1137. #stats_dashboard_widget_control .button-primary {
  1138. float: right;
  1139. }
  1140. #dashboard_stats {
  1141. box-sizing: border-box;
  1142. width: 100%;
  1143. padding: 0 10px;
  1144. }
  1145. </style>
  1146. <?php
  1147. }
  1148. /**
  1149. * Register Stats Widget Control Callback.
  1150. *
  1151. * @access public
  1152. * @return void
  1153. */
  1154. function stats_register_widget_control_callback() {
  1155. $GLOBALS['wp_dashboard_control_callbacks']['dashboard_stats'] = 'stats_dashboard_widget_control';
  1156. }
  1157. /**
  1158. * JavaScript and CSS for dashboard widget.
  1159. *
  1160. * @access public
  1161. * @return void
  1162. */
  1163. function stats_dashboard_head() { ?>
  1164. <script type="text/javascript">
  1165. /* <![CDATA[ */
  1166. jQuery( function($) {
  1167. var dashStats = jQuery( '#dashboard_stats div.inside' );
  1168. if ( dashStats.find( '.dashboard-widget-control-form' ).length ) {
  1169. return;
  1170. }
  1171. if ( ! dashStats.length ) {
  1172. dashStats = jQuery( '#dashboard_stats div.dashboard-widget-content' );
  1173. var h = parseInt( dashStats.parent().height() ) - parseInt( dashStats.prev().height() );
  1174. var args = 'width=' + dashStats.width() + '&height=' + h.toString();
  1175. } else {
  1176. if ( jQuery('#dashboard_stats' ).hasClass('postbox') ) {
  1177. var args = 'width=' + ( dashStats.prev().width() * 2 ).toString();
  1178. } else {
  1179. var args = 'width=' + ( dashStats.width() * 2 ).toString();
  1180. }
  1181. }
  1182. dashStats
  1183. .not( '.dashboard-widget-control' )
  1184. .load( 'admin.php?page=stats&noheader&dashboard&' + args );
  1185. jQuery( window ).one( 'resize', function() {
  1186. jQuery( '#stat-chart' ).css( 'width', 'auto' );
  1187. } );
  1188. } );
  1189. /* ]]> */
  1190. </script>
  1191. <style type="text/css">
  1192. /* <![CDATA[ */
  1193. #stat-chart {
  1194. background: none !important;
  1195. }
  1196. #dashboard_stats .inside {
  1197. margin: 10px 0 0 0 !important;
  1198. }
  1199. #dashboard_stats #stats-graph {
  1200. margin: 0;
  1201. }
  1202. #stats-info {
  1203. border-top: 1px solid #dfdfdf;
  1204. margin: 7px -10px 0 -10px;
  1205. padding: 10px;
  1206. background: #fcfcfc;
  1207. -moz-box-shadow:inset 0 1px 0 #fff;
  1208. -webkit-box-shadow:inset 0 1px 0 #fff;
  1209. box-shadow:inset 0 1px 0 #fff;
  1210. overflow: hidden;
  1211. border-radius: 0 0 2px 2px;
  1212. -webkit-border-radius: 0 0 2px 2px;
  1213. -moz-border-radius: 0 0 2px 2px;
  1214. -khtml-border-radius: 0 0 2px 2px;
  1215. }
  1216. #stats-info #top-posts, #stats-info #top-search {
  1217. float: left;
  1218. width: 50%;
  1219. }
  1220. #stats-info #top-posts {
  1221. padding-right: 3%;
  1222. }
  1223. #top-posts .stats-section-inner p {
  1224. white-space: nowrap;
  1225. overflow: hidden;
  1226. }
  1227. #top-posts .stats-section-inner p a {
  1228. overflow: hidden;
  1229. text-overflow: ellipsis;
  1230. }
  1231. #stats-info div#active {
  1232. border-top: 1px solid #dfdfdf;
  1233. margin: 0 -10px;
  1234. padding: 10px 10px 0 10px;
  1235. -moz-box-shadow:inset 0 1px 0 #fff;
  1236. -webkit-box-shadow:inset 0 1px 0 #fff;
  1237. box-shadow:inset 0 1px 0 #fff;
  1238. overflow: hidden;
  1239. }
  1240. #top-search p {
  1241. color: #999;
  1242. }
  1243. #stats-info h3 {
  1244. font-size: 1em;
  1245. margin: 0 0 .5em 0 !important;
  1246. }
  1247. #stats-info p {
  1248. margin: 0 0 .25em;
  1249. color: #999;
  1250. }
  1251. #stats-info p.widget-loading {
  1252. margin: 1em 0 0;
  1253. color: #333;
  1254. }
  1255. #stats-info p a {
  1256. display: block;
  1257. }
  1258. #stats-info p a.button {
  1259. display: inline;
  1260. }
  1261. /* ]]> */
  1262. </style>
  1263. <?php
  1264. }
  1265. /**
  1266. * Stats Dashboard Widget Content.
  1267. *
  1268. * @access public
  1269. * @return void
  1270. */
  1271. function stats_dashboard_widget_content() {
  1272. if ( ! isset( $_GET['width'] ) || ( ! $width = (int) ( $_GET['width'] / 2 ) ) || $width < 250 ) {
  1273. $width = 370;
  1274. }
  1275. if ( ! isset( $_GET['height'] ) || ( ! $height = (int) $_GET['height'] - 36 ) || $height < 230 ) {
  1276. $height = 180;
  1277. }
  1278. $_width = $width - 5;
  1279. $_height = $height - ( $GLOBALS['is_winIE'] ? 16 : 5 ); // Hack!
  1280. $options = stats_dashboard_widget_options();
  1281. $blog_id = Jetpack_Options::get_option( 'id' );
  1282. $q = array(
  1283. 'noheader' => 'true',
  1284. 'proxy' => '',
  1285. 'blog' => $blog_id,
  1286. 'page' => 'stats',
  1287. 'chart' => '',
  1288. 'unit' => $options['chart'],
  1289. 'color' => get_user_option( 'admin_color' ),
  1290. 'width' => $_width,
  1291. 'height' => $_height,
  1292. 'ssl' => is_ssl(),
  1293. 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
  1294. );
  1295. $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-admin/index.php";
  1296. $url = add_query_arg( $q, $url );
  1297. $method = 'GET';
  1298. $timeout = 90;
  1299. $user_id = JETPACK_MASTER_USER;
  1300. $get = Jetpack_Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
  1301. $get_code = wp_remote_retrieve_response_code( $get );
  1302. if ( is_wp_error( $get ) || ( 2 !== intval( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
  1303. stats_print_wp_remote_error( $get, $url );
  1304. } else {
  1305. $body = stats_convert_post_titles( $get['body'] );
  1306. $body = stats_convert_chart_urls( $body );
  1307. $body = stats_convert_image_urls( $body );
  1308. echo $body;
  1309. }
  1310. $post_ids = array();
  1311. $csv_end_date = date( 'Y-m-d', current_time( 'timestamp' ) );
  1312. $csv_args = array( 'top' => "&limit=8&end=$csv_end_date", 'search' => "&limit=5&end=$csv_end_date" );
  1313. /* Translators: Stats dashboard widget postviews list: "$post_title $views Views". */
  1314. $printf = __( '%1$s %2$s Views' , 'jetpack' );
  1315. foreach ( $top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" ) as $i => $post ) {
  1316. if ( 0 === $post['post_id'] ) {
  1317. unset( $top_posts[$i] );
  1318. continue;
  1319. }
  1320. $post_ids[] = $post['post_id'];
  1321. }
  1322. // Cache.
  1323. get_posts( array( 'include' => join( ',', array_unique( $post_ids ) ) ) );
  1324. $searches = array();
  1325. foreach ( $search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" ) as $search_term ) {
  1326. if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
  1327. continue;
  1328. }
  1329. $searches[] = esc_html( $search_term['searchterm'] );
  1330. }
  1331. ?>
  1332. <a class="button" href="admin.php?page=stats"><?php esc_html_e( 'View All', 'jetpack' ); ?></a>
  1333. <div id="stats-info">
  1334. <div id="top-posts" class='stats-section'>
  1335. <div class="stats-section-inner">
  1336. <h3 class="heading"><?php esc_html_e( 'Top Posts' , 'jetpack' ); ?></h3>
  1337. <?php
  1338. if ( empty( $top_posts ) ) {
  1339. ?>
  1340. <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
  1341. <?php
  1342. } else {
  1343. foreach ( $top_posts as $post ) {
  1344. if ( ! get_post( $post['post_id'] ) ) {
  1345. continue;
  1346. }
  1347. ?>
  1348. <p><?php printf(
  1349. $printf,
  1350. '<a href="' . get_permalink( $post['post_id'] ) . '">' . get_the_title( $post['post_id'] ) . '</a>',
  1351. number_format_i18n( $post['views'] )
  1352. ); ?></p>
  1353. <?php
  1354. }
  1355. }
  1356. ?>
  1357. </div>
  1358. </div>
  1359. <div id="top-search" class='stats-section'>
  1360. <div class="stats-section-inner">
  1361. <h3 class="heading"><?php esc_html_e( 'Top Searches' , 'jetpack' ); ?></h3>
  1362. <?php
  1363. if ( empty( $searches ) ) {
  1364. ?>
  1365. <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
  1366. <?php
  1367. } else {
  1368. ?>
  1369. <p><?php echo join( ',&nbsp; ', $searches );?></p>
  1370. <?php
  1371. }
  1372. ?>
  1373. </div>
  1374. </div>
  1375. </div>
  1376. <div class="clear"></div>
  1377. <?php
  1378. exit;
  1379. }
  1380. /**
  1381. * Stats Print WP Remote Error.
  1382. *
  1383. * @access public
  1384. * @param mixed $get Get.
  1385. * @param mixed $url URL.
  1386. * @return void
  1387. */
  1388. function stats_print_wp_remote_error( $get, $url ) {
  1389. $state_name = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
  1390. $previous_error = Jetpack::state( $state_name );
  1391. $error = md5( serialize( compact( 'get', 'url' ) ) );
  1392. Jetpack::state( $state_name, $error );
  1393. if ( $error !== $previous_error ) {
  1394. ?>
  1395. <div class="wrap">
  1396. <p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
  1397. </div>
  1398. <?php
  1399. return;
  1400. }
  1401. ?>
  1402. <div class="wrap">
  1403. <p><?php printf( __( 'We were unable to get your stats just now. Please reload this page to try again. If this error persists, please <a href="%1$s" target="_blank">contact support</a>. In your report please include the information below.', 'jetpack' ), 'https://support.wordpress.com/contact/?jetpack=needs-service' ); ?></p>
  1404. <pre>
  1405. User Agent: "<?php echo esc_html( $_SERVER['HTTP_USER_AGENT'] ); ?>"
  1406. Page URL: "http<?php echo (is_ssl()?'s':'') . '://' . esc_html( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); ?>"
  1407. API URL: "<?php echo esc_url( $url ); ?>"
  1408. <?php
  1409. if ( is_wp_error( $get ) ) {
  1410. foreach ( $get->get_error_codes() as $code ) {
  1411. foreach ( $get->get_error_messages( $code ) as $message ) {
  1412. ?>
  1413. <?php print $code . ': "' . $message . '"' ?>
  1414. <?php
  1415. }
  1416. }
  1417. } else {
  1418. $get_code = wp_remote_retrieve_response_code( $get );
  1419. $content_length = strlen( wp_remote_retrieve_body( $get ) );
  1420. ?>
  1421. Response code: "<?php print $get_code ?>"
  1422. Content length: "<?php print $content_length ?>"
  1423. <?php
  1424. }
  1425. ?></pre>
  1426. </div>
  1427. <?php
  1428. }
  1429. /**
  1430. * Get stats from WordPress.com
  1431. *
  1432. * @param string $table The stats which you want to retrieve: postviews, or searchterms.
  1433. * @param array $args {
  1434. * An associative array of arguments.
  1435. *
  1436. * @type bool $end The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
  1437. * and default timezone is UTC date. Default value is Now.
  1438. * @type string $days The length of the desired time frame. Default is 30. Maximum 90 days.
  1439. * @type int $limit The maximum number of records to return. Default is 10. Maximum 100.
  1440. * @type int $post_id The ID of the post to retrieve stats data for
  1441. * @type string $summarize If present, summarizes all matching records. Default Null.
  1442. *
  1443. * }
  1444. *
  1445. * @return array {
  1446. * An array of post view data, each post as an array
  1447. *
  1448. * array {
  1449. * The post view data for a single post
  1450. *
  1451. * @type string $post_id The ID of the post
  1452. * @type string $post_title The title of the post
  1453. * @type string $post_permalink The permalink for the post
  1454. * @type string $views The number of views for the post within the $num_days specified
  1455. * }
  1456. * }
  1457. */
  1458. function stats_get_csv( $table, $args = null ) {
  1459. $defaults = array( 'end' => false, 'days' => false, 'limit' => 3, 'post_id' => false, 'summarize' => '' );
  1460. $args = wp_parse_args( $args, $defaults );
  1461. $args['table'] = $table;
  1462. $args['blog_id'] = Jetpack_Options::get_option( 'id' );
  1463. $stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
  1464. $key = md5( $stats_csv_url );
  1465. // Get cache.
  1466. $stats_cache = get_option( 'stats_cache' );
  1467. if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
  1468. $stats_cache = array();
  1469. }
  1470. // Return or expire this key.
  1471. if ( isset( $stats_cache[ $key ] ) ) {
  1472. $time = key( $stats_cache[ $key ] );
  1473. if ( time() - $time < 300 ) {
  1474. return $stats_cache[ $key ][ $time ];
  1475. }
  1476. unset( $stats_cache[ $key ] );
  1477. }
  1478. $stats_rows = array();
  1479. do {
  1480. if ( ! $stats = stats_get_remote_csv( $stats_csv_url ) ) {
  1481. break;
  1482. }
  1483. $labels = array_shift( $stats );
  1484. if ( 0 === stripos( $labels[0], 'error' ) ) {
  1485. break;
  1486. }
  1487. $stats_rows = array();
  1488. for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
  1489. $row = array();
  1490. foreach ( $labels as $col => $label ) {
  1491. $row[ $label ] = $stats[ $s ][ $col ];
  1492. }
  1493. $stats_rows[] = $row;
  1494. }
  1495. } while ( 0 );
  1496. // Expire old keys.
  1497. foreach ( $stats_cache as $k => $cache ) {
  1498. if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
  1499. unset( $stats_cache[ $k ] );
  1500. }
  1501. }
  1502. // Set cache.
  1503. $stats_cache[ $key ] = array( time() => $stats_rows );
  1504. update_option( 'stats_cache', $stats_cache );
  1505. return $stats_rows;
  1506. }
  1507. /**
  1508. * Stats get remote CSV.
  1509. *
  1510. * @access public
  1511. * @param mixed $url URL.
  1512. * @return array
  1513. */
  1514. function stats_get_remote_csv( $url ) {
  1515. $method = 'GET';
  1516. $timeout = 90;
  1517. $user_id = JETPACK_MASTER_USER;
  1518. $get = Jetpack_Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
  1519. $get_code = wp_remote_retrieve_response_code( $get );
  1520. if ( is_wp_error( $get ) || ( 2 !== intval( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
  1521. return array(); // @todo: return an error?
  1522. } else {
  1523. return stats_str_getcsv( $get['body'] );
  1524. }
  1525. }
  1526. /**
  1527. * Rather than parsing the csv and its special cases, we create a new file and do fgetcsv on it.
  1528. *
  1529. * @access public
  1530. * @param mixed $csv CSV.
  1531. * @return array.
  1532. */
  1533. function stats_str_getcsv( $csv ) {
  1534. if ( function_exists( 'str_getcsv' ) ) {
  1535. $lines = str_getcsv( $csv, "\n" ); // phpcs:ignore PHPCompatibility
  1536. return array_map( 'str_getcsv', $lines );
  1537. }
  1538. if ( ! $temp = tmpfile() ) { // The tmpfile() automatically unlinks.
  1539. return false;
  1540. }
  1541. $data = array();
  1542. fwrite( $temp, $csv, strlen( $csv ) );
  1543. fseek( $temp, 0 );
  1544. while ( false !== $row = fgetcsv( $temp, 2000 ) ) {
  1545. $data[] = $row;
  1546. }
  1547. fclose( $temp );
  1548. return $data;
  1549. }
  1550. /**
  1551. * Abstract out building the rest api stats path.
  1552. *
  1553. * @param string $resource Resource.
  1554. * @return string
  1555. */
  1556. function jetpack_stats_api_path( $resource = '' ) {
  1557. $resource = ltrim( $resource, '/' );
  1558. return sprintf( '/sites/%d/stats/%s', stats_get_option( 'blog_id' ), $resource );
  1559. }
  1560. /**
  1561. * Fetches stats data from the REST API. Caches locally for 5 minutes.
  1562. *
  1563. * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
  1564. * @access public
  1565. * @param array $args (default: array()) The args that are passed to the endpoint.
  1566. * @param string $resource (default: '') Optional sub-endpoint following /stats/.
  1567. * @return array|WP_Error.
  1568. */
  1569. function stats_get_from_restapi( $args = array(), $resource = '' ) {
  1570. $endpoint = jetpack_stats_api_path( $resource );
  1571. $api_version = '1.1';
  1572. $args = wp_parse_args( $args, array() );
  1573. $cache_key = md5( implode( '|', array( $endpoint, $api_version, serialize( $args ) ) ) );
  1574. // Get cache.
  1575. $stats_cache = Jetpack_Options::get_option( 'restapi_stats_cache', array() );
  1576. if ( ! is_array( $stats_cache ) ) {
  1577. $stats_cache = array();
  1578. }
  1579. // Return or expire this key.
  1580. if ( isset( $stats_cache[ $cache_key ] ) ) {
  1581. $time = key( $stats_cache[ $cache_key ] );
  1582. if ( time() - $time < ( 5 * MINUTE_IN_SECONDS ) ) {
  1583. $cached_stats = $stats_cache[ $cache_key ][ $time ];
  1584. $cached_stats = (object) array_merge( array( 'cached_at' => $time ), (array) $cached_stats );
  1585. return $cached_stats;
  1586. }
  1587. unset( $stats_cache[ $cache_key ] );
  1588. }
  1589. // Do the dirty work.
  1590. $response = Jetpack_Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
  1591. if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
  1592. // If bad, just return it, don't cache.
  1593. return $response;
  1594. }
  1595. $data = json_decode( wp_remote_retrieve_body( $response ) );
  1596. // Expire old keys.
  1597. foreach ( $stats_cache as $k => $cache ) {
  1598. if ( ! is_array( $cache ) || ( 5 * MINUTE_IN_SECONDS ) < time() - key( $cache ) ) {
  1599. unset( $stats_cache[ $k ] );
  1600. }
  1601. }
  1602. // Set cache.
  1603. $stats_cache[ $cache_key ] = array(
  1604. time() => $data,
  1605. );
  1606. Jetpack_Options::update_option( 'restapi_stats_cache', $stats_cache, false );
  1607. return $data;
  1608. }
  1609. /**
  1610. * Load CSS needed for Stats column width in WP-Admin area.
  1611. *
  1612. * @since 4.7.0
  1613. */
  1614. function jetpack_stats_load_admin_css() {
  1615. ?>
  1616. <style type="text/css">
  1617. .fixed .column-stats {
  1618. width: 5em;
  1619. }
  1620. </style>
  1621. <?php
  1622. }
  1623. /**
  1624. * Set header for column that allows to go to WordPress.com to see an entry's stats.
  1625. *
  1626. * @param array $columns An array of column names.
  1627. *
  1628. * @since 4.7.0
  1629. *
  1630. * @return mixed
  1631. */
  1632. function jetpack_stats_post_table( $columns ) { // Adds a stats link on the edit posts page
  1633. if ( ! current_user_can( 'view_stats' ) || ! Jetpack::is_user_connected() ) {
  1634. return $columns;
  1635. }
  1636. // Array-Fu to add before comments
  1637. $pos = array_search( 'comments', array_keys( $columns ) );
  1638. if ( ! is_int( $pos ) ) {
  1639. return $columns;
  1640. }
  1641. $chunks = array_chunk( $columns, $pos, true );
  1642. $chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack' );
  1643. return call_user_func_array( 'array_merge', $chunks );
  1644. }
  1645. /**
  1646. * Set content for cell with link to an entry's stats in WordPress.com.
  1647. *
  1648. * @param string $column The name of the column to display.
  1649. * @param int $post_id The current post ID.
  1650. *
  1651. * @since 4.7.0
  1652. *
  1653. * @return mixed
  1654. */
  1655. function jetpack_stats_post_table_cell( $column, $post_id ) {
  1656. if ( 'stats' == $column ) {
  1657. if ( 'publish' != get_post_status( $post_id ) ) {
  1658. printf(
  1659. '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
  1660. esc_html__( 'No stats', 'jetpack' )
  1661. );
  1662. } else {
  1663. printf(
  1664. '<a href="%s" title="%s" class="dashicons dashicons-chart-bar" target="_blank"></a>',
  1665. esc_url( "https://wordpress.com/stats/post/$post_id/" . Jetpack::build_raw_urls( get_home_url() ) ),
  1666. esc_html__( 'View stats for this post in WordPress.com', 'jetpack' )
  1667. );
  1668. }
  1669. }
  1670. }