Aucune description

stats.php 47KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695
  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 automattic/jetpack
  15. */
  16. use Automattic\Jetpack\Connection\Client;
  17. use Automattic\Jetpack\Connection\Manager as Connection_Manager;
  18. use Automattic\Jetpack\Connection\XMLRPC_Async_Call;
  19. use Automattic\Jetpack\Redirect;
  20. use Automattic\Jetpack\Status;
  21. use Automattic\Jetpack\Tracking;
  22. if ( defined( 'STATS_VERSION' ) ) {
  23. return;
  24. }
  25. define( 'STATS_VERSION', '9' );
  26. defined( 'STATS_DASHBOARD_SERVER' ) || define( 'STATS_DASHBOARD_SERVER', 'dashboard.wordpress.com' );
  27. add_action( 'jetpack_modules_loaded', 'stats_load' );
  28. /**
  29. * Load Stats.
  30. *
  31. * @access public
  32. * @return void
  33. */
  34. function stats_load() {
  35. Jetpack::enable_module_configurable( __FILE__ );
  36. // Generate the tracking code after wp() has queried for posts.
  37. add_action( 'template_redirect', 'stats_template_redirect', 1 );
  38. add_action( 'wp_head', 'stats_admin_bar_head', 100 );
  39. add_action( 'wp_head', 'stats_hide_smile_css' );
  40. add_action( 'embed_head', 'stats_hide_smile_css' );
  41. add_action( 'jetpack_admin_menu', 'stats_admin_menu' );
  42. // Map stats caps.
  43. add_filter( 'map_meta_cap', 'stats_map_meta_caps', 10, 3 );
  44. add_action( 'admin_init', 'stats_merged_widget_admin_init' );
  45. add_filter( 'jetpack_xmlrpc_unauthenticated_methods', 'stats_xmlrpc_methods' );
  46. add_filter( 'pre_option_db_version', 'stats_ignore_db_version' );
  47. // Add an icon to see stats in WordPress.com for a particular post.
  48. add_action( 'admin_print_styles-edit.php', 'jetpack_stats_load_admin_css' );
  49. add_filter( 'manage_posts_columns', 'jetpack_stats_post_table' );
  50. add_filter( 'manage_pages_columns', 'jetpack_stats_post_table' );
  51. add_action( 'manage_posts_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
  52. add_action( 'manage_pages_custom_column', 'jetpack_stats_post_table_cell', 10, 2 );
  53. require_once __DIR__ . '/stats/class-jetpack-stats-upgrade-nudges.php';
  54. add_action( 'updating_jetpack_version', array( 'Jetpack_Stats_Upgrade_Nudges', 'unset_nudges_setting' ) );
  55. }
  56. /**
  57. * Delay conditional for current_user_can to after init.
  58. *
  59. * @access public
  60. * @return void
  61. */
  62. function stats_merged_widget_admin_init() {
  63. if ( current_user_can( 'view_stats' ) ) {
  64. add_action( 'load-index.php', 'stats_enqueue_dashboard_head' );
  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 === (int) $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'] && // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  113. isset( $_GET['chart'] ) && strpos( $_GET['chart'], 'admin-bar-hours' ) === 0 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  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, true ) ) {
  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;
  150. if ( is_feed() || is_robots() || is_trackback() || is_preview() || jetpack_is_dnt_enabled() ) {
  151. return;
  152. }
  153. // Staging Sites should not generate tracking stats.
  154. $status = new Status();
  155. if ( $status->is_staging_site() ) {
  156. return;
  157. }
  158. // Should we be counting this user's views?
  159. if ( ! empty( $current_user->ID ) ) {
  160. $count_roles = stats_get_option( 'count_roles' );
  161. if ( ! is_array( $count_roles ) || ! array_intersect( $current_user->roles, $count_roles ) ) {
  162. return;
  163. }
  164. }
  165. add_action( 'wp_footer', 'stats_footer', 101 );
  166. add_action( 'web_stories_print_analytics', 'stats_footer' );
  167. }
  168. /**
  169. * Stats Build View Data.
  170. *
  171. * @access public
  172. * @return array.
  173. */
  174. function stats_build_view_data() {
  175. global $wp_the_query;
  176. $blog = Jetpack_Options::get_option( 'id' );
  177. $tz = get_option( 'gmt_offset' );
  178. $v = 'ext';
  179. $blog_url = wp_parse_url( site_url() );
  180. $srv = $blog_url['host'];
  181. $j = sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION );
  182. if ( $wp_the_query->is_single || $wp_the_query->is_page || $wp_the_query->is_posts_page ) {
  183. // Store and reset the queried_object and queried_object_id
  184. // Otherwise, redirect_canonical() will redirect to home_url( '/' ) for show_on_front = page sites where home_url() is not all lowercase.
  185. // Repro:
  186. // 1. Set home_url = https://ExamPle.com/
  187. // 2. Set show_on_front = page
  188. // 3. Set page_on_front = something
  189. // 4. Visit https://example.com/ !
  190. $queried_object = isset( $wp_the_query->queried_object ) ? $wp_the_query->queried_object : null;
  191. $queried_object_id = isset( $wp_the_query->queried_object_id ) ? $wp_the_query->queried_object_id : null;
  192. try {
  193. $post_obj = $wp_the_query->get_queried_object();
  194. $post = $post_obj instanceof WP_Post ? $post_obj->ID : '0';
  195. } finally {
  196. $wp_the_query->queried_object = $queried_object;
  197. $wp_the_query->queried_object_id = $queried_object_id;
  198. }
  199. } else {
  200. $post = '0';
  201. }
  202. return compact( 'v', 'j', 'blog', 'post', 'tz', 'srv' );
  203. }
  204. /**
  205. * Stats Footer.
  206. *
  207. * @access public
  208. * @return void
  209. */
  210. function stats_footer() {
  211. $data = stats_build_view_data();
  212. if ( Jetpack_AMP_Support::is_amp_request() ) {
  213. stats_render_amp_footer( $data );
  214. } else {
  215. stats_render_footer( $data );
  216. }
  217. }
  218. /**
  219. * Render the stats footer
  220. *
  221. * @param array $data Array of data for the JS stats tracker.
  222. */
  223. function stats_render_footer( $data ) {
  224. // phpcs:disable WordPress.WP.EnqueuedResources.NonEnqueuedScript
  225. // When there is a way to use defer with enqueue, we can move to it and inline the custom data.
  226. $script = 'https://stats.wp.com/e-' . gmdate( 'YW' ) . '.js';
  227. $data_stats_array = stats_array( $data );
  228. $stats_footer = <<<END
  229. <script src='{$script}' defer></script>
  230. <script>
  231. _stq = window._stq || [];
  232. _stq.push([ 'view', {{$data_stats_array}} ]);
  233. _stq.push([ 'clickTrackerInit', '{$data['blog']}', '{$data['post']}' ]);
  234. </script>
  235. END;
  236. // phpcs:enable
  237. print $stats_footer; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  238. }
  239. /**
  240. * Render the stats footer for AMP output.
  241. *
  242. * @param array $data Array of data for the JS stats tracker.
  243. */
  244. function stats_render_amp_footer( $data ) {
  245. $data['host'] = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // input var ok.
  246. $data['rand'] = 'RANDOM'; // AMP placeholder.
  247. $data['ref'] = 'DOCUMENT_REFERRER'; // AMP placeholder.
  248. $data = array_map( 'rawurlencode', $data );
  249. $pixel_url = add_query_arg( $data, 'https://pixel.wp.com/g.gif' );
  250. ?>
  251. <amp-pixel src="<?php echo esc_url( $pixel_url ); ?>"></amp-pixel>
  252. <?php
  253. }
  254. /**
  255. * Stats Get Options.
  256. *
  257. * @access public
  258. * @return array.
  259. */
  260. function stats_get_options() {
  261. $options = get_option( 'stats_options' );
  262. if ( ! isset( $options['version'] ) || $options['version'] < STATS_VERSION ) {
  263. $options = stats_upgrade_options( $options );
  264. }
  265. return $options;
  266. }
  267. /**
  268. * Get Stats Options.
  269. *
  270. * @access public
  271. * @param mixed $option Option.
  272. * @return mixed|null.
  273. */
  274. function stats_get_option( $option ) {
  275. $options = stats_get_options();
  276. if ( 'blog_id' === $option ) {
  277. return Jetpack_Options::get_option( 'id' );
  278. }
  279. if ( isset( $options[ $option ] ) ) {
  280. return $options[ $option ];
  281. }
  282. return null;
  283. }
  284. /**
  285. * Stats Set Options.
  286. *
  287. * @access public
  288. * @param mixed $option Option.
  289. * @param mixed $value Value.
  290. * @return bool.
  291. */
  292. function stats_set_option( $option, $value ) {
  293. $options = stats_get_options();
  294. $options[ $option ] = $value;
  295. return stats_set_options( $options );
  296. }
  297. /**
  298. * Stats Set Options.
  299. *
  300. * @access public
  301. * @param mixed $options Options.
  302. * @return bool
  303. */
  304. function stats_set_options( $options ) {
  305. return update_option( 'stats_options', $options );
  306. }
  307. /**
  308. * Stats Upgrade Options.
  309. *
  310. * @access public
  311. * @param mixed $options Options.
  312. * @return array|bool
  313. */
  314. function stats_upgrade_options( $options ) {
  315. $defaults = array(
  316. 'admin_bar' => true,
  317. 'roles' => array( 'administrator' ),
  318. 'count_roles' => array(),
  319. 'blog_id' => Jetpack_Options::get_option( 'id' ),
  320. 'do_not_track' => true, // @todo
  321. 'hide_smile' => true,
  322. );
  323. if ( isset( $options['reg_users'] ) ) {
  324. if ( ! function_exists( 'get_editable_roles' ) ) {
  325. require_once ABSPATH . 'wp-admin/includes/user.php';
  326. }
  327. if ( $options['reg_users'] ) {
  328. $options['count_roles'] = array_keys( get_editable_roles() );
  329. }
  330. unset( $options['reg_users'] );
  331. }
  332. if ( is_array( $options ) && ! empty( $options ) ) {
  333. $new_options = array_merge( $defaults, $options );
  334. } else {
  335. $new_options = $defaults;
  336. }
  337. $new_options['version'] = STATS_VERSION;
  338. if ( ! stats_set_options( $new_options ) ) {
  339. return false;
  340. }
  341. stats_update_blog();
  342. return $new_options;
  343. }
  344. /**
  345. * Creates the "array" string used as part of the JS tracker.
  346. *
  347. * @access public
  348. * @param array $kvs KVS.
  349. * @return string
  350. */
  351. function stats_array( $kvs ) {
  352. /**
  353. * Filter the options added to the JavaScript Stats tracking code.
  354. *
  355. * @module stats
  356. *
  357. * @since 1.1.0
  358. *
  359. * @param array $kvs Array of options about the site and page you're on.
  360. */
  361. $kvs = (array) apply_filters( 'stats_array', $kvs );
  362. $kvs = array_map( 'addslashes', $kvs );
  363. $jskvs = array();
  364. foreach ( $kvs as $k => $v ) {
  365. $jskvs[] = "$k:'$v'";
  366. }
  367. return join( ',', $jskvs );
  368. }
  369. /**
  370. * Admin Pages.
  371. *
  372. * @access public
  373. * @return void
  374. */
  375. function stats_admin_menu() {
  376. global $pagenow;
  377. // If we're at an old Stats URL, redirect to the new one.
  378. // Don't even bother with caps, menu_page_url(), etc. Just do it.
  379. if ( 'index.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  380. $redirect_url = str_replace( array( '/wp-admin/index.php?', '/wp-admin/?' ), '/wp-admin/admin.php?', $_SERVER['REQUEST_URI'] );
  381. $relative_pos = strpos( $redirect_url, '/wp-admin/' );
  382. if ( false !== $relative_pos ) {
  383. wp_safe_redirect( admin_url( substr( $redirect_url, $relative_pos + 10 ) ) );
  384. exit;
  385. }
  386. }
  387. $hook = add_submenu_page( 'jetpack', __( 'Site Stats', 'jetpack' ), __( 'Site Stats', 'jetpack' ), 'view_stats', 'stats', 'jetpack_admin_ui_stats_report_page_wrapper' );
  388. add_action( "load-$hook", 'stats_reports_load' );
  389. }
  390. /**
  391. * Stats Admin Path.
  392. *
  393. * @access public
  394. * @return string
  395. */
  396. function stats_admin_path() {
  397. return Jetpack::module_configuration_url( __FILE__ );
  398. }
  399. /**
  400. * Stats Reports Load.
  401. *
  402. * @access public
  403. * @return void
  404. */
  405. function stats_reports_load() {
  406. require_once __DIR__ . '/stats/class-jetpack-stats-upgrade-nudges.php';
  407. Jetpack_Stats_Upgrade_Nudges::init();
  408. wp_enqueue_script( 'jquery' );
  409. wp_enqueue_script( 'postbox' );
  410. wp_enqueue_script( 'underscore' );
  411. Jetpack_Admin_Page::load_wrapper_styles();
  412. add_action( 'admin_print_styles', 'stats_reports_css' );
  413. if ( isset( $_GET['nojs'] ) && $_GET['nojs'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  414. $parsed = wp_parse_url( admin_url() );
  415. // Remember user doesn't want JS.
  416. setcookie( 'stnojs', '1', time() + 172800, $parsed['path'] ); // 2 days.
  417. }
  418. if ( isset( $_COOKIE['stnojs'] ) && $_COOKIE['stnojs'] ) {
  419. // Detect if JS is on. If so, remove cookie so next page load is via JS.
  420. add_action( 'admin_print_footer_scripts', 'stats_js_remove_stnojs_cookie' );
  421. } elseif ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  422. // Normal page load. Load page content via JS.
  423. add_action( 'admin_print_footer_scripts', 'stats_js_load_page_via_ajax' );
  424. }
  425. }
  426. /**
  427. * Stats Reports CSS.
  428. *
  429. * @access public
  430. * @return void
  431. */
  432. function stats_reports_css() {
  433. ?>
  434. <style type="text/css">
  435. #jp-stats-wrap, #jp-stats-report-bottom {
  436. max-width: 1040px;
  437. margin: 0 auto;
  438. overflow: hidden;
  439. }
  440. #stats-loading-wrap p {
  441. text-align: center;
  442. font-size: 2em;
  443. margin: 7.5em 15px 0 0;
  444. height: 64px;
  445. line-height: 64px;
  446. }
  447. </style>
  448. <?php
  449. }
  450. /**
  451. * Detect if JS is on. If so, remove cookie so next page load is via JS.
  452. *
  453. * @access public
  454. * @return void
  455. */
  456. function stats_js_remove_stnojs_cookie() {
  457. $parsed = wp_parse_url( admin_url() );
  458. ?>
  459. <script type="text/javascript">
  460. /* <![CDATA[ */
  461. document.cookie = 'stnojs=0; expires=Wed, 9 Mar 2011 16:55:50 UTC; path=<?php echo esc_js( $parsed['path'] ); ?>';
  462. /* ]]> */
  463. </script>
  464. <?php
  465. }
  466. /**
  467. * Normal page load. Load page content via JS.
  468. *
  469. * @access public
  470. * @return void
  471. */
  472. function stats_js_load_page_via_ajax() {
  473. ?>
  474. <script type="text/javascript">
  475. /* <![CDATA[ */
  476. if ( -1 == document.location.href.indexOf( 'noheader' ) ) {
  477. jQuery( function( $ ) {
  478. $.get( document.location.href + '&noheader', function( responseText ) {
  479. $( '#stats-loading-wrap' ).replaceWith( responseText );
  480. $( '#jp-stats-wrap' )[0].dispatchEvent( new Event( 'stats-loaded' ) );
  481. } );
  482. } );
  483. }
  484. /* ]]> */
  485. </script>
  486. <?php
  487. }
  488. /**
  489. * Jetpack Admin Page Wrapper.
  490. */
  491. function jetpack_admin_ui_stats_report_page_wrapper() {
  492. if ( ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  493. Jetpack_Admin_Page::wrap_ui( 'stats_reports_page', array( 'is-wide' => true ) );
  494. } else {
  495. stats_reports_page();
  496. }
  497. }
  498. /**
  499. * Stats Report Page.
  500. *
  501. * @access public
  502. * @param bool $main_chart_only (default: false) Main Chart Only.
  503. */
  504. function stats_reports_page( $main_chart_only = false ) {
  505. if ( isset( $_GET['dashboard'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  506. return stats_dashboard_widget_content();
  507. }
  508. $blog_id = stats_get_option( 'blog_id' );
  509. $stats_url = Redirect::get_url( 'calypso-stats' );
  510. if ( ! $main_chart_only && ! isset( $_GET['noheader'] ) && empty( $_GET['nojs'] ) && empty( $_COOKIE['stnojs'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  511. $nojs_url = add_query_arg( 'nojs', '1' );
  512. $http = is_ssl() ? 'https' : 'http';
  513. // Loading message. No JS fallback message.
  514. ?>
  515. <div id="jp-stats-wrap">
  516. <div class="wrap">
  517. <h2><?php esc_html_e( 'Site Stats', 'jetpack' ); ?>
  518. <?php
  519. if ( current_user_can( 'jetpack_manage_modules' ) ) :
  520. $i18n_headers = jetpack_get_module_i18n( 'stats' );
  521. ?>
  522. <a
  523. style="font-size:13px;"
  524. href="<?php echo esc_url( admin_url( 'admin.php?page=jetpack#/settings?term=' . rawurlencode( $i18n_headers['name'] ) ) ); ?>"
  525. >
  526. <?php esc_html_e( 'Configure', 'jetpack' ); ?>
  527. </a>
  528. <?php
  529. endif;
  530. /**
  531. * Sets external resource URL.
  532. *
  533. * @module stats
  534. *
  535. * @since 1.4.0
  536. * @todo Clean up various uses of this filter. It's seemingly filtering different types of images in different places.
  537. *
  538. * @param string $args URL of external resource.
  539. */
  540. $static_url = apply_filters( 'jetpack_static_url', "{$http}://en.wordpress.com/i/loading/loading-64.gif" );
  541. ?>
  542. </h2>
  543. </div>
  544. <div id="stats-loading-wrap" class="wrap">
  545. <p class="hide-if-no-js"><img width="32" height="32" alt="<?php esc_attr_e( 'Loading&hellip;', 'jetpack' ); ?>" src="<?php echo esc_url( $static_url ); ?>" /></p>
  546. <p style="font-size: 11pt; margin: 0;"><a href="<?php echo esc_url( $stats_url ); ?>" rel="noopener noreferrer" target="_blank"><?php esc_html_e( 'View stats on WordPress.com right now', 'jetpack' ); ?></a></p>
  547. <p class="hide-if-js"><?php esc_html_e( 'Your Site Stats work better with JavaScript enabled.', 'jetpack' ); ?><br />
  548. <a href="<?php echo esc_url( $nojs_url ); ?>"><?php esc_html_e( 'View Site Stats without JavaScript', 'jetpack' ); ?></a>.</p>
  549. </div>
  550. </div>
  551. <?php
  552. return;
  553. }
  554. $day = isset( $_GET['day'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_GET['day'] ) ? $_GET['day'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  555. $q = array(
  556. 'noheader' => 'true',
  557. 'proxy' => '',
  558. 'page' => 'stats',
  559. 'day' => $day,
  560. 'blog' => $blog_id,
  561. 'charset' => get_option( 'blog_charset' ),
  562. 'color' => get_user_option( 'admin_color' ),
  563. 'ssl' => is_ssl(),
  564. 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
  565. );
  566. if ( get_locale() !== 'en_US' ) {
  567. $q['jp_lang'] = get_locale();
  568. }
  569. // Only show the main chart, without extra header data, or metaboxes.
  570. $q['main_chart_only'] = $main_chart_only;
  571. $args = array(
  572. 'view' => array( 'referrers', 'postviews', 'searchterms', 'clicks', 'post', 'table' ),
  573. 'numdays' => 'int',
  574. 'day' => 'date',
  575. 'unit' => array( '1', '7', '31', 'human' ),
  576. 'humanize' => array( 'true' ),
  577. 'num' => 'int',
  578. 'summarize' => null,
  579. 'post' => 'int',
  580. 'width' => 'int',
  581. 'height' => 'int',
  582. 'data' => 'data',
  583. 'blog_subscribers' => 'int',
  584. 'comment_subscribers' => null,
  585. 'type' => array( 'wpcom', 'email', 'pending' ),
  586. 'pagenum' => 'int',
  587. );
  588. foreach ( $args as $var => $vals ) {
  589. if ( ! isset( $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  590. continue;
  591. }
  592. if ( is_array( $vals ) ) {
  593. if ( in_array( $_REQUEST[ $var ], $vals, true ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  594. $q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  595. }
  596. } elseif ( 'int' === $vals ) {
  597. $q[ $var ] = (int) $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  598. } elseif ( 'date' === $vals ) {
  599. if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $_REQUEST[ $var ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  600. $q[ $var ] = $_REQUEST[ $var ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  601. }
  602. } elseif ( null === $vals ) {
  603. $q[ $var ] = '';
  604. } elseif ( 'data' === $vals ) {
  605. if ( 'index.php' === substr( $_REQUEST[ $var ], 0, 9 ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  606. $q[ $var ] = $_REQUEST[ $var ];// phpcs:ignore WordPress.Security.NonceVerification.Recommended
  607. }
  608. }
  609. }
  610. if ( isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  611. if ( preg_match( '/^[a-z0-9-]+$/', $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  612. $chart = sanitize_title( $_GET['chart'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  613. $url = 'https://' . STATS_DASHBOARD_SERVER . "/wp-includes/charts/{$chart}.php";
  614. }
  615. } else {
  616. $url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
  617. }
  618. $url = add_query_arg( $q, $url );
  619. $method = 'GET';
  620. $timeout = 90;
  621. $user_id = 0; // Means use the blog token.
  622. $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
  623. $get_code = wp_remote_retrieve_response_code( $get );
  624. if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
  625. stats_print_wp_remote_error( $get, $url );
  626. } else {
  627. if ( ! empty( $get['headers']['content-type'] ) ) {
  628. $type = $get['headers']['content-type'];
  629. if ( substr( $type, 0, 5 ) === 'image' ) {
  630. $img = $get['body'];
  631. header( 'Content-Type: ' . $type );
  632. header( 'Content-Length: ' . strlen( $img ) );
  633. echo $img; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  634. die();
  635. }
  636. }
  637. $body = stats_convert_post_titles( $get['body'] );
  638. $body = stats_convert_chart_urls( $body );
  639. $body = stats_convert_image_urls( $body );
  640. $body = stats_convert_admin_urls( $body );
  641. echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  642. }
  643. if ( isset( $_GET['page'] ) && 'stats' === $_GET['page'] && ! isset( $_GET['chart'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  644. $tracking = new Tracking();
  645. $tracking->record_user_event( 'wpa_page_view', array( 'path' => 'old_stats' ) );
  646. }
  647. if ( isset( $_GET['noheader'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  648. die;
  649. }
  650. }
  651. /**
  652. * Stats Convert Admin Urls.
  653. *
  654. * @access public
  655. * @param mixed $html HTML.
  656. * @return string
  657. */
  658. function stats_convert_admin_urls( $html ) {
  659. return str_replace( 'index.php?page=stats', 'admin.php?page=stats', $html );
  660. }
  661. /**
  662. * Stats Convert Image URLs.
  663. *
  664. * @access public
  665. * @param mixed $html HTML.
  666. * @return string
  667. */
  668. function stats_convert_image_urls( $html ) {
  669. $url = set_url_scheme( 'https://' . STATS_DASHBOARD_SERVER );
  670. $html = preg_replace( '|(["\'])(/i/stats.+)\\1|', '$1' . $url . '$2$1', $html );
  671. return $html;
  672. }
  673. /**
  674. * Callback for preg_replace_callback used in stats_convert_chart_urls()
  675. *
  676. * @since 5.6.0
  677. *
  678. * @param array $matches The matches resulting from the preg_replace_callback call.
  679. * @return string The admin url for the chart.
  680. */
  681. function jetpack_stats_convert_chart_urls_callback( $matches ) {
  682. // If there is a query string, change the beginning '?' to a '&' so it fits into the middle of this query string.
  683. return 'admin.php?page=stats&noheader&chart=' . $matches[1] . str_replace( '?', '&', $matches[2] );
  684. }
  685. /**
  686. * Stats Convert Chart URLs.
  687. *
  688. * @access public
  689. * @param mixed $html HTML.
  690. * @return string
  691. */
  692. function stats_convert_chart_urls( $html ) {
  693. $html = preg_replace_callback(
  694. '|https?://[-.a-z0-9]+/wp-includes/charts/([-.a-z0-9]+).php(\??)|',
  695. 'jetpack_stats_convert_chart_urls_callback',
  696. $html
  697. );
  698. return $html;
  699. }
  700. /**
  701. * Stats Convert Post Title HTML
  702. *
  703. * @access public
  704. * @param mixed $html HTML.
  705. * @return string
  706. */
  707. function stats_convert_post_titles( $html ) {
  708. global $stats_posts;
  709. $pattern = "<span class='post-(\d+)-link'>.*?</span>";
  710. if ( ! preg_match_all( "!$pattern!", $html, $matches ) ) {
  711. return $html;
  712. }
  713. $posts = get_posts(
  714. array(
  715. 'include' => implode( ',', $matches[1] ),
  716. 'post_type' => 'any',
  717. 'post_status' => 'any',
  718. 'numberposts' => -1,
  719. 'suppress_filters' => false,
  720. )
  721. );
  722. foreach ( $posts as $post ) {
  723. $stats_posts[ $post->ID ] = $post;
  724. }
  725. $html = preg_replace_callback( "!$pattern!", 'stats_convert_post_title', $html );
  726. return $html;
  727. }
  728. /**
  729. * Stats Convert Post Title Matches.
  730. *
  731. * @access public
  732. * @param mixed $matches Matches.
  733. * @return string
  734. */
  735. function stats_convert_post_title( $matches ) {
  736. global $stats_posts;
  737. $post_id = $matches[1];
  738. if ( isset( $stats_posts[ $post_id ] ) ) {
  739. return '<a href="' . get_permalink( $post_id ) . '" target="_blank">' . get_the_title( $post_id ) . '</a>';
  740. }
  741. return $matches[0];
  742. }
  743. /**
  744. * Stats Hide Smile.
  745. *
  746. * @access public
  747. * @return void
  748. */
  749. function stats_hide_smile_css() {
  750. $options = stats_get_options();
  751. if ( isset( $options['hide_smile'] ) && $options['hide_smile'] ) {
  752. ?>
  753. <style type='text/css'>img#wpstats{display:none}</style>
  754. <?php
  755. }
  756. }
  757. /**
  758. * Stats Admin Bar Head.
  759. *
  760. * @access public
  761. * @return void
  762. */
  763. function stats_admin_bar_head() {
  764. if ( ! stats_get_option( 'admin_bar' ) ) {
  765. return;
  766. }
  767. if ( ! current_user_can( 'view_stats' ) ) {
  768. return;
  769. }
  770. if ( ! is_admin_bar_showing() ) {
  771. return;
  772. }
  773. add_action( 'admin_bar_menu', 'stats_admin_bar_menu', 100 );
  774. ?>
  775. <style data-ampdevmode type='text/css'>
  776. #wpadminbar .quicklinks li#wp-admin-bar-stats {
  777. height: 32px;
  778. }
  779. #wpadminbar .quicklinks li#wp-admin-bar-stats a {
  780. height: 32px;
  781. padding: 0;
  782. }
  783. #wpadminbar .quicklinks li#wp-admin-bar-stats a div {
  784. height: 32px;
  785. width: 95px;
  786. overflow: hidden;
  787. margin: 0 10px;
  788. }
  789. #wpadminbar .quicklinks li#wp-admin-bar-stats a:hover div {
  790. width: auto;
  791. margin: 0 8px 0 10px;
  792. }
  793. #wpadminbar .quicklinks li#wp-admin-bar-stats a img {
  794. height: 24px;
  795. margin: 4px 0;
  796. max-width: none;
  797. border: none;
  798. }
  799. </style>
  800. <?php
  801. }
  802. /**
  803. * Stats AdminBar.
  804. *
  805. * @access public
  806. * @param mixed $wp_admin_bar WPAdminBar.
  807. * @return void
  808. */
  809. function stats_admin_bar_menu( &$wp_admin_bar ) {
  810. $url = add_query_arg( 'page', 'stats', admin_url( 'admin.php' ) ); // no menu_page_url() blog-side.
  811. $img_src = esc_attr(
  812. add_query_arg(
  813. array(
  814. 'noheader' => '',
  815. 'proxy' => '',
  816. 'chart' => 'admin-bar-hours-scale',
  817. ),
  818. $url
  819. )
  820. );
  821. $img_src_2x = esc_attr(
  822. add_query_arg(
  823. array(
  824. 'noheader' => '',
  825. 'proxy' => '',
  826. 'chart' => 'admin-bar-hours-scale-2x',
  827. ),
  828. $url
  829. )
  830. );
  831. $alt = esc_attr( __( 'Stats', 'jetpack' ) );
  832. $title = esc_attr( __( 'Views over 48 hours. Click for more Site Stats.', 'jetpack' ) );
  833. $menu = array(
  834. 'id' => 'stats',
  835. 'href' => $url,
  836. 'title' => "<div><img src='$img_src' srcset='$img_src 1x, $img_src_2x 2x' width='112' height='24' alt='$alt' title='$title'></div>",
  837. );
  838. $wp_admin_bar->add_menu( $menu );
  839. }
  840. /**
  841. * Stats Update Blog.
  842. *
  843. * @access public
  844. * @return void
  845. */
  846. function stats_update_blog() {
  847. XMLRPC_Async_Call::add_call( 'jetpack.updateBlog', 0, stats_get_blog() );
  848. }
  849. /**
  850. * Stats Get Blog.
  851. *
  852. * @access public
  853. * @return string
  854. */
  855. function stats_get_blog() {
  856. $home = wp_parse_url( trailingslashit( get_option( 'home' ) ) );
  857. $blog = array(
  858. 'host' => $home['host'],
  859. 'path' => $home['path'],
  860. 'blogname' => get_option( 'blogname' ),
  861. 'blogdescription' => get_option( 'blogdescription' ),
  862. 'siteurl' => get_option( 'siteurl' ),
  863. 'gmt_offset' => get_option( 'gmt_offset' ),
  864. 'timezone_string' => get_option( 'timezone_string' ),
  865. 'stats_version' => STATS_VERSION,
  866. 'stats_api' => 'jetpack',
  867. 'page_on_front' => get_option( 'page_on_front' ),
  868. 'permalink_structure' => get_option( 'permalink_structure' ),
  869. 'category_base' => get_option( 'category_base' ),
  870. 'tag_base' => get_option( 'tag_base' ),
  871. );
  872. $blog = array_merge( stats_get_options(), $blog );
  873. unset( $blog['roles'], $blog['blog_id'] );
  874. return stats_esc_html_deep( $blog );
  875. }
  876. /**
  877. * Modified from stripslashes_deep()
  878. *
  879. * @access public
  880. * @param mixed $value Value.
  881. * @return string
  882. */
  883. function stats_esc_html_deep( $value ) {
  884. if ( is_array( $value ) ) {
  885. $value = array_map( 'stats_esc_html_deep', $value );
  886. } elseif ( is_object( $value ) ) {
  887. $vars = get_object_vars( $value );
  888. foreach ( $vars as $key => $data ) {
  889. $value->{$key} = stats_esc_html_deep( $data );
  890. }
  891. } elseif ( is_string( $value ) ) {
  892. $value = esc_html( $value );
  893. }
  894. return $value;
  895. }
  896. /**
  897. * Stats xmlrpc_methods function.
  898. *
  899. * @access public
  900. * @param mixed $methods Methods.
  901. * @return array
  902. */
  903. function stats_xmlrpc_methods( $methods ) {
  904. $my_methods = array(
  905. 'jetpack.getBlog' => 'stats_get_blog',
  906. );
  907. return array_merge( $methods, $my_methods );
  908. }
  909. /**
  910. * Stats Dashboard Widget Options.
  911. *
  912. * @access public
  913. * @return array
  914. */
  915. function stats_dashboard_widget_options() {
  916. $defaults = array(
  917. 'chart' => 1,
  918. 'top' => 1,
  919. 'search' => 7,
  920. );
  921. $options = get_option( 'stats_dashboard_widget' );
  922. if ( ( ! $options ) || ! is_array( $options ) ) {
  923. $options = array();
  924. }
  925. // Ignore obsolete option values.
  926. $intervals = array( 1, 7, 31, 90, 365 );
  927. foreach ( array( 'top', 'search' ) as $key ) {
  928. if ( isset( $options[ $key ] ) && ! in_array( (int) $options[ $key ], $intervals, true ) ) {
  929. unset( $options[ $key ] );
  930. }
  931. }
  932. return array_merge( $defaults, $options );
  933. }
  934. /**
  935. * Stats Dashboard Widget Control.
  936. *
  937. * @access public
  938. * @return void
  939. */
  940. function stats_dashboard_widget_control() {
  941. $periods = array(
  942. '1' => __( 'day', 'jetpack' ),
  943. '7' => __( 'week', 'jetpack' ),
  944. '31' => __( 'month', 'jetpack' ),
  945. );
  946. $intervals = array(
  947. '1' => __( 'the past day', 'jetpack' ),
  948. '7' => __( 'the past week', 'jetpack' ),
  949. '31' => __( 'the past month', 'jetpack' ),
  950. '90' => __( 'the past quarter', 'jetpack' ),
  951. '365' => __( 'the past year', 'jetpack' ),
  952. );
  953. $defaults = array(
  954. 'top' => 1,
  955. 'search' => 7,
  956. );
  957. $options = stats_dashboard_widget_options();
  958. if ( 'post' === strtolower( $_SERVER['REQUEST_METHOD'] ) && isset( $_POST['widget_id'] ) && 'dashboard_stats' === $_POST['widget_id'] ) { // phpcs:ignore WordPress.Security.NonceVerification
  959. if ( isset( $periods[ $_POST['chart'] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
  960. $options['chart'] = $_POST['chart']; // phpcs:ignore WordPress.Security.NonceVerification
  961. }
  962. foreach ( array( 'top', 'search' ) as $key ) {
  963. if ( isset( $intervals[ $_POST[ $key ] ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
  964. $options[ $key ] = $_POST[ $key ]; // phpcs:ignore WordPress.Security.NonceVerification
  965. } else {
  966. $options[ $key ] = $defaults[ $key ];
  967. }
  968. }
  969. update_option( 'stats_dashboard_widget', $options );
  970. }
  971. ?>
  972. <p>
  973. <label for="chart"><?php esc_html_e( 'Chart stats by', 'jetpack' ); ?></label>
  974. <select id="chart" name="chart">
  975. <?php
  976. foreach ( $periods as $val => $label ) {
  977. ?>
  978. <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['chart'] ); ?>><?php echo esc_html( $label ); ?></option>
  979. <?php
  980. }
  981. ?>
  982. </select>.
  983. </p>
  984. <p>
  985. <label for="top"><?php esc_html_e( 'Show top posts over', 'jetpack' ); ?></label>
  986. <select id="top" name="top">
  987. <?php
  988. foreach ( $intervals as $val => $label ) {
  989. ?>
  990. <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['top'] ); ?>><?php echo esc_html( $label ); ?></option>
  991. <?php
  992. }
  993. ?>
  994. </select>.
  995. </p>
  996. <p>
  997. <label for="search"><?php esc_html_e( 'Show top search terms over', 'jetpack' ); ?></label>
  998. <select id="search" name="search">
  999. <?php
  1000. foreach ( $intervals as $val => $label ) {
  1001. ?>
  1002. <option value="<?php echo esc_attr( $val ); ?>"<?php selected( $val, $options['search'] ); ?>><?php echo esc_html( $label ); ?></option>
  1003. <?php
  1004. }
  1005. ?>
  1006. </select>.
  1007. </p>
  1008. <?php
  1009. }
  1010. /**
  1011. * Jetpack Stats Dashboard Widget.
  1012. *
  1013. * @access public
  1014. * @return void
  1015. */
  1016. function stats_jetpack_dashboard_widget() {
  1017. ?>
  1018. <form id="stats_dashboard_widget_control" action="<?php echo esc_url( admin_url() ); ?>" method="post">
  1019. <?php stats_dashboard_widget_control(); ?>
  1020. <?php wp_nonce_field( 'edit-dashboard-widget_dashboard_stats', 'dashboard-widget-nonce' ); ?>
  1021. <input type="hidden" name="widget_id" value="dashboard_stats" />
  1022. <?php submit_button( __( 'Submit', 'jetpack' ) ); ?>
  1023. </form>
  1024. <button type="button" class="handlediv js-toggle-stats_dashboard_widget_control" aria-expanded="true">
  1025. <span class="screen-reader-text"><?php esc_html_e( 'Configure', 'jetpack' ); ?></span>
  1026. <span class="toggle-indicator" aria-hidden="true"></span>
  1027. </button>
  1028. <div id="dashboard_stats">
  1029. <div class="inside">
  1030. <div style="height: 250px;"></div>
  1031. </div>
  1032. </div>
  1033. <?php
  1034. }
  1035. /**
  1036. * JavaScript and CSS for dashboard widget.
  1037. *
  1038. * @access public
  1039. * @return void
  1040. */
  1041. function stats_dashboard_head() {
  1042. ?>
  1043. <script type="text/javascript">
  1044. /* <![CDATA[ */
  1045. jQuery( function($) {
  1046. var dashStats = jQuery( '#dashboard_stats div.inside' );
  1047. if ( dashStats.find( '.dashboard-widget-control-form' ).length ) {
  1048. return;
  1049. }
  1050. if ( ! dashStats.length ) {
  1051. dashStats = jQuery( '#dashboard_stats div.dashboard-widget-content' );
  1052. var h = parseInt( dashStats.parent().height() ) - parseInt( dashStats.prev().height() );
  1053. var args = 'width=' + dashStats.width() + '&height=' + h.toString();
  1054. } else {
  1055. if ( jQuery('#dashboard_stats' ).hasClass('postbox') ) {
  1056. var args = 'width=' + ( dashStats.prev().width() * 2 ).toString();
  1057. } else {
  1058. var args = 'width=' + ( dashStats.width() * 2 ).toString();
  1059. }
  1060. }
  1061. dashStats
  1062. .not( '.dashboard-widget-control' )
  1063. .load( 'admin.php?page=stats&noheader&dashboard&' + args );
  1064. jQuery( window ).one( 'resize', function() {
  1065. jQuery( '#stat-chart' ).css( 'width', 'auto' );
  1066. } );
  1067. // Widget settings toggle container.
  1068. var toggle = $( '.js-toggle-stats_dashboard_widget_control' );
  1069. // Move the toggle in the widget header.
  1070. toggle.appendTo( '#jetpack_summary_widget .handle-actions' );
  1071. // Toggle settings when clicking on it.
  1072. toggle.show().click( function( e ) {
  1073. e.preventDefault();
  1074. e.stopImmediatePropagation();
  1075. $( this ).parent().toggleClass( 'controlVisible' );
  1076. $( '#stats_dashboard_widget_control' ).slideToggle();
  1077. } );
  1078. } );
  1079. /* ]]> */
  1080. </script>
  1081. <?php
  1082. }
  1083. /**
  1084. * Stats Dashboard Widget Content.
  1085. *
  1086. * @access public
  1087. * @return void
  1088. */
  1089. function stats_dashboard_widget_content() {
  1090. $width = isset( $_GET['width'] ) ? (int) ( $_GET['width'] / 2 ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  1091. $height = isset( $_GET['height'] ) ? (int) $_GET['height'] - 36 : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  1092. if ( ! $width || $width < 250 ) {
  1093. $width = 370;
  1094. }
  1095. if ( ! $height || $height < 230 ) {
  1096. $height = 180;
  1097. }
  1098. $_width = $width - 5;
  1099. $_height = $height - ( $GLOBALS['is_winIE'] ? 16 : 5 ); // Hack! @todo Remove WordPress 5.8 is minimum. IE should be fully deprecated.
  1100. $options = stats_dashboard_widget_options();
  1101. $blog_id = Jetpack_Options::get_option( 'id' );
  1102. $q = array(
  1103. 'noheader' => 'true',
  1104. 'proxy' => '',
  1105. 'blog' => $blog_id,
  1106. 'page' => 'stats',
  1107. 'chart' => '',
  1108. 'unit' => $options['chart'],
  1109. 'color' => get_user_option( 'admin_color' ),
  1110. 'width' => $_width,
  1111. 'height' => $_height,
  1112. 'ssl' => is_ssl(),
  1113. 'j' => sprintf( '%s:%s', JETPACK__API_VERSION, JETPACK__VERSION ),
  1114. );
  1115. $url = 'https://' . STATS_DASHBOARD_SERVER . '/wp-admin/index.php';
  1116. $url = add_query_arg( $q, $url );
  1117. $method = 'GET';
  1118. $timeout = 90;
  1119. $user_id = 0; // Means use the blog token.
  1120. $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
  1121. $get_code = wp_remote_retrieve_response_code( $get );
  1122. if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
  1123. stats_print_wp_remote_error( $get, $url );
  1124. } else {
  1125. $body = stats_convert_post_titles( $get['body'] );
  1126. $body = stats_convert_chart_urls( $body );
  1127. $body = stats_convert_image_urls( $body );
  1128. echo $body; // phpcs:ignore WordPress.Security.EscapeOutput
  1129. }
  1130. $post_ids = array();
  1131. $csv_end_date = current_time( 'Y-m-d' );
  1132. $csv_args = array(
  1133. 'top' => "&limit=8&end=$csv_end_date",
  1134. 'search' => "&limit=5&end=$csv_end_date",
  1135. );
  1136. $top_posts = stats_get_csv( 'postviews', "days=$options[top]$csv_args[top]" );
  1137. foreach ( $top_posts as $i => $post ) {
  1138. if ( 0 === $post['post_id'] ) {
  1139. unset( $top_posts[ $i ] );
  1140. continue;
  1141. }
  1142. $post_ids[] = $post['post_id'];
  1143. }
  1144. // Cache.
  1145. get_posts( array( 'include' => join( ',', array_unique( $post_ids ) ) ) );
  1146. $searches = array();
  1147. $search_terms = stats_get_csv( 'searchterms', "days=$options[search]$csv_args[search]" );
  1148. foreach ( $search_terms as $search_term ) {
  1149. if ( 'encrypted_search_terms' === $search_term['searchterm'] ) {
  1150. continue;
  1151. }
  1152. $searches[] = esc_html( $search_term['searchterm'] );
  1153. }
  1154. ?>
  1155. <div id="stats-info">
  1156. <div id="top-posts" class='stats-section'>
  1157. <div class="stats-section-inner">
  1158. <h3 class="heading"><?php esc_html_e( 'Top Posts', 'jetpack' ); ?></h3>
  1159. <?php
  1160. if ( empty( $top_posts ) ) {
  1161. ?>
  1162. <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
  1163. <?php
  1164. } else {
  1165. foreach ( $top_posts as $post ) {
  1166. if ( ! get_post( $post['post_id'] ) ) {
  1167. continue;
  1168. }
  1169. ?>
  1170. <p>
  1171. <?php
  1172. printf(
  1173. esc_html(
  1174. /* Translators: Stats dashboard widget Post list with view count: "Post Title 1 View (or Views if plural)". */
  1175. _n( '%1$s %2$s View', '%1$s %2$s Views', $post['views'], 'jetpack' )
  1176. ),
  1177. '<a href="' . esc_url( get_permalink( $post['post_id'] ) ) . '">' . esc_html( get_the_title( $post['post_id'] ) ) . '</a>',
  1178. esc_html( number_format_i18n( $post['views'] ) )
  1179. );
  1180. ?>
  1181. </p>
  1182. <?php
  1183. }
  1184. }
  1185. ?>
  1186. </div>
  1187. </div>
  1188. <div id="top-search" class='stats-section'>
  1189. <div class="stats-section-inner">
  1190. <h3 class="heading"><?php esc_html_e( 'Top Searches', 'jetpack' ); ?></h3>
  1191. <?php
  1192. if ( empty( $searches ) ) {
  1193. ?>
  1194. <p class="nothing"><?php esc_html_e( 'Sorry, nothing to report.', 'jetpack' ); ?></p>
  1195. <?php
  1196. } else {
  1197. foreach ( $searches as $search_term_item ) {
  1198. printf(
  1199. '<p>%s</p>',
  1200. esc_html( $search_term_item )
  1201. );
  1202. }
  1203. }
  1204. ?>
  1205. </div>
  1206. </div>
  1207. </div>
  1208. <div class="clear"></div>
  1209. <div class="stats-view-all">
  1210. <?php
  1211. $stats_day_url = Redirect::get_url( 'calypso-stats-day' );
  1212. printf(
  1213. '<a class="button" target="_blank" rel="noopener noreferrer" href="%1$s">%2$s</a>',
  1214. esc_url( $stats_day_url ),
  1215. esc_html__( 'View all stats', 'jetpack' )
  1216. );
  1217. ?>
  1218. </div>
  1219. <div class="clear"></div>
  1220. <?php
  1221. exit;
  1222. }
  1223. /**
  1224. * Stats Print WP Remote Error.
  1225. *
  1226. * @access public
  1227. * @param mixed $get Get.
  1228. * @param mixed $url URL.
  1229. * @return void
  1230. */
  1231. function stats_print_wp_remote_error( $get, $url ) {
  1232. $state_name = 'stats_remote_error_' . substr( md5( $url ), 0, 8 );
  1233. $previous_error = Jetpack::state( $state_name );
  1234. $error = md5( wp_json_encode( compact( 'get', 'url' ) ) );
  1235. Jetpack::state( $state_name, $error );
  1236. if ( $error !== $previous_error ) {
  1237. ?>
  1238. <div class="wrap">
  1239. <p><?php esc_html_e( 'We were unable to get your stats just now. Please reload this page to try again.', 'jetpack' ); ?></p>
  1240. </div>
  1241. <?php
  1242. return;
  1243. }
  1244. ?>
  1245. <div class="wrap">
  1246. <p>
  1247. <?php
  1248. printf(
  1249. /* translators: placeholder is an a href for a support site. */
  1250. esc_html__( 'We were unable to get your stats just now. Please reload this page to try again. If this error persists, please contact %1$s. In your report, please include the information below.', 'jetpack' ),
  1251. sprintf(
  1252. '<a href="https://support.wordpress.com/contact/?jetpack=needs-service">%s</a>',
  1253. esc_html__( 'Jetpack Support', 'jetpack' )
  1254. )
  1255. );
  1256. ?>
  1257. </p>
  1258. <pre>
  1259. User Agent: "<?php echo esc_html( $_SERVER['HTTP_USER_AGENT'] ); ?>"
  1260. Page URL: "http<?php echo ( is_ssl() ? 's' : '' ) . '://' . esc_html( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ); ?>"
  1261. API URL: "<?php echo esc_url( $url ); ?>"
  1262. <?php
  1263. if ( is_wp_error( $get ) ) {
  1264. foreach ( $get->get_error_codes() as $code ) {
  1265. foreach ( $get->get_error_messages( $code ) as $message ) {
  1266. ?>
  1267. <?php print esc_html( $code ) . ': "' . esc_html( $message ) . '"'; ?>
  1268. <?php
  1269. }
  1270. }
  1271. } else {
  1272. $get_code = wp_remote_retrieve_response_code( $get );
  1273. $content_length = strlen( wp_remote_retrieve_body( $get ) );
  1274. ?>
  1275. Response code: "<?php print esc_html( $get_code ); ?>"
  1276. Content length: "<?php print esc_html( $content_length ); ?>"
  1277. <?php
  1278. }
  1279. ?>
  1280. </pre>
  1281. </div>
  1282. <?php
  1283. }
  1284. /**
  1285. * Get stats from WordPress.com
  1286. *
  1287. * @param string $table The stats which you want to retrieve: postviews, or searchterms.
  1288. * @param array $args {
  1289. * An associative array of arguments.
  1290. *
  1291. * @type bool $end The last day of the desired time frame. Format is 'Y-m-d' (e.g. 2007-05-01)
  1292. * and default timezone is UTC date. Default value is Now.
  1293. * @type string $days The length of the desired time frame. Default is 30. Maximum 90 days.
  1294. * @type int $limit The maximum number of records to return. Default is 10. Maximum 100.
  1295. * @type int $post_id The ID of the post to retrieve stats data for
  1296. * @type string $summarize If present, summarizes all matching records. Default Null.
  1297. *
  1298. * }
  1299. *
  1300. * @return array {
  1301. * An array of post view data, each post as an array
  1302. *
  1303. * array {
  1304. * The post view data for a single post
  1305. *
  1306. * @type string $post_id The ID of the post
  1307. * @type string $post_title The title of the post
  1308. * @type string $post_permalink The permalink for the post
  1309. * @type string $views The number of views for the post within the $num_days specified
  1310. * }
  1311. * }
  1312. */
  1313. function stats_get_csv( $table, $args = null ) {
  1314. $defaults = array(
  1315. 'end' => false,
  1316. 'days' => false,
  1317. 'limit' => 3,
  1318. 'post_id' => false,
  1319. 'summarize' => '',
  1320. );
  1321. $args = wp_parse_args( $args, $defaults );
  1322. $args['table'] = $table;
  1323. $args['blog_id'] = Jetpack_Options::get_option( 'id' );
  1324. $stats_csv_url = add_query_arg( $args, 'https://stats.wordpress.com/csv.php' );
  1325. $key = md5( $stats_csv_url );
  1326. // Get cache.
  1327. $stats_cache = get_option( 'stats_cache' );
  1328. if ( ! $stats_cache || ! is_array( $stats_cache ) ) {
  1329. $stats_cache = array();
  1330. }
  1331. // Return or expire this key.
  1332. if ( isset( $stats_cache[ $key ] ) ) {
  1333. $time = key( $stats_cache[ $key ] );
  1334. if ( time() - $time < 300 ) {
  1335. return $stats_cache[ $key ][ $time ];
  1336. }
  1337. unset( $stats_cache[ $key ] );
  1338. }
  1339. $stats_rows = array();
  1340. do {
  1341. $stats = stats_get_remote_csv( $stats_csv_url );
  1342. if ( ! $stats ) {
  1343. break;
  1344. }
  1345. $labels = array_shift( $stats );
  1346. if ( 0 === stripos( $labels[0], 'error' ) ) {
  1347. break;
  1348. }
  1349. $stats_rows = array();
  1350. for ( $s = 0; isset( $stats[ $s ] ); $s++ ) {
  1351. $row = array();
  1352. foreach ( $labels as $col => $label ) {
  1353. $row[ $label ] = $stats[ $s ][ $col ];
  1354. }
  1355. $stats_rows[] = $row;
  1356. }
  1357. } while ( 0 );
  1358. // Expire old keys.
  1359. foreach ( $stats_cache as $k => $cache ) {
  1360. if ( ! is_array( $cache ) || 300 < time() - key( $cache ) ) {
  1361. unset( $stats_cache[ $k ] );
  1362. }
  1363. }
  1364. // Set cache.
  1365. $stats_cache[ $key ] = array( time() => $stats_rows );
  1366. update_option( 'stats_cache', $stats_cache );
  1367. return $stats_rows;
  1368. }
  1369. /**
  1370. * Stats get remote CSV.
  1371. *
  1372. * @access public
  1373. * @param mixed $url URL.
  1374. * @return array
  1375. */
  1376. function stats_get_remote_csv( $url ) {
  1377. $method = 'GET';
  1378. $timeout = 90;
  1379. $user_id = 0; // Blog token.
  1380. $get = Client::remote_request( compact( 'url', 'method', 'timeout', 'user_id' ) );
  1381. $get_code = wp_remote_retrieve_response_code( $get );
  1382. if ( is_wp_error( $get ) || ( 2 !== (int) ( $get_code / 100 ) && 304 !== $get_code ) || empty( $get['body'] ) ) {
  1383. return array(); // @todo: return an error?
  1384. } else {
  1385. return stats_str_getcsv( $get['body'] );
  1386. }
  1387. }
  1388. /**
  1389. * Recursively run str_getcsv on the stats csv.
  1390. *
  1391. * @since 9.7.0 Remove custom handling since str_getcsv is available on all servers running this now.
  1392. *
  1393. * @param mixed $csv CSV.
  1394. * @return array.
  1395. */
  1396. function stats_str_getcsv( $csv ) {
  1397. $lines = str_getcsv( $csv, "\n" );
  1398. return array_map( 'str_getcsv', $lines );
  1399. }
  1400. /**
  1401. * Abstract out building the rest api stats path.
  1402. *
  1403. * @param string $resource Resource.
  1404. * @return string
  1405. */
  1406. function jetpack_stats_api_path( $resource = '' ) {
  1407. $resource = ltrim( $resource, '/' );
  1408. return sprintf( '/sites/%d/stats/%s', stats_get_option( 'blog_id' ), $resource );
  1409. }
  1410. /**
  1411. * Fetches stats data from the REST API. Caches locally for 5 minutes.
  1412. *
  1413. * @link: https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/stats/
  1414. * @access public
  1415. * @param array $args (default: array()) The args that are passed to the endpoint.
  1416. * @param string $resource (default: '') Optional sub-endpoint following /stats/.
  1417. * @return array|WP_Error.
  1418. */
  1419. function stats_get_from_restapi( $args = array(), $resource = '' ) {
  1420. $endpoint = jetpack_stats_api_path( $resource );
  1421. $api_version = '1.1';
  1422. $args = wp_parse_args( $args, array() );
  1423. $cache_key = md5( implode( '|', array( $endpoint, $api_version, wp_json_encode( $args ) ) ) );
  1424. $transient_name = "jetpack_restapi_stats_cache_{$cache_key}";
  1425. $stats_cache = get_transient( $transient_name );
  1426. // Return or expire this key.
  1427. if ( $stats_cache ) {
  1428. $time = key( $stats_cache );
  1429. $data = $stats_cache[ $time ]; // WP_Error or string (JSON encoded object).
  1430. if ( is_wp_error( $data ) ) {
  1431. return $data;
  1432. }
  1433. return (object) array_merge( array( 'cached_at' => $time ), (array) json_decode( $data ) );
  1434. }
  1435. // Do the dirty work.
  1436. $response = Client::wpcom_json_api_request_as_blog( $endpoint, $api_version, $args );
  1437. if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
  1438. // WP_Error.
  1439. $data = is_wp_error( $response ) ? $response : new WP_Error( 'stats_error' );
  1440. // WP_Error.
  1441. $return = $data;
  1442. } else {
  1443. // string (JSON encoded object).
  1444. $data = wp_remote_retrieve_body( $response );
  1445. // object (rare: null on JSON failure).
  1446. $return = json_decode( $data );
  1447. }
  1448. // To reduce size in storage: store with time as key, store JSON encoded data (unless error).
  1449. set_transient( $transient_name, array( time() => $data ), 5 * MINUTE_IN_SECONDS );
  1450. return $return;
  1451. }
  1452. /**
  1453. * Load CSS needed for Stats column width in WP-Admin area.
  1454. *
  1455. * @since 4.7.0
  1456. */
  1457. function jetpack_stats_load_admin_css() {
  1458. ?>
  1459. <style type="text/css">
  1460. .fixed .column-stats {
  1461. width: 5em;
  1462. }
  1463. </style>
  1464. <?php
  1465. }
  1466. /**
  1467. * Set header for column that allows to go to WordPress.com to see an entry's stats.
  1468. *
  1469. * @param array $columns An array of column names.
  1470. *
  1471. * @since 4.7.0
  1472. *
  1473. * @return mixed
  1474. */
  1475. function jetpack_stats_post_table( $columns ) {
  1476. // Adds a stats link on the edit posts page.
  1477. if ( ! current_user_can( 'view_stats' ) || ! ( new Connection_Manager( 'jetpack' ) )->is_user_connected() ) {
  1478. return $columns;
  1479. }
  1480. // Array-Fu to add before comments.
  1481. $pos = array_search( 'comments', array_keys( $columns ), true );
  1482. if ( ! is_int( $pos ) ) {
  1483. return $columns;
  1484. }
  1485. $chunks = array_chunk( $columns, $pos, true );
  1486. $chunks[0]['stats'] = esc_html__( 'Stats', 'jetpack' );
  1487. return call_user_func_array( 'array_merge', $chunks );
  1488. }
  1489. /**
  1490. * Set content for cell with link to an entry's stats in WordPress.com.
  1491. *
  1492. * @param string $column The name of the column to display.
  1493. * @param int $post_id The current post ID.
  1494. *
  1495. * @since 4.7.0
  1496. *
  1497. * @return mixed
  1498. */
  1499. function jetpack_stats_post_table_cell( $column, $post_id ) {
  1500. if ( 'stats' === $column ) {
  1501. if ( 'publish' !== get_post_status( $post_id ) ) {
  1502. printf(
  1503. '<span aria-hidden="true">—</span><span class="screen-reader-text">%s</span>',
  1504. esc_html__( 'No stats', 'jetpack' )
  1505. );
  1506. } else {
  1507. $stats_post_url = Redirect::get_url(
  1508. 'calypso-stats-post',
  1509. array(
  1510. 'path' => $post_id,
  1511. )
  1512. );
  1513. printf(
  1514. '<a href="%s" title="%s" class="dashicons dashicons-chart-bar" target="_blank"></a>',
  1515. esc_url( $stats_post_url ),
  1516. esc_html__( 'View stats for this post in WordPress.com', 'jetpack' )
  1517. );
  1518. }
  1519. }
  1520. }