<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[2985] sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory: Plugin Directory: Add an internal API endpoint which will be used to update the stats stored within postmeta.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="http://meta.trac.wordpress.org/changeset/2985">2985</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"http://meta.trac.wordpress.org/changeset/2985","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>dd32</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2016-04-20 11:08:56 +0000 (Wed, 20 Apr 2016)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Plugin Directory: Add an internal API endpoint which will be used to update the stats stored within postmeta.
Currently other w.org systems have to query the DB directly, bypassing WordPress actions and filters, which results in stale data or data not being passed to another service.

See <a href="http://meta.trac.wordpress.org/ticket/1596">#1596</a></pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclassplugindirectoryphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclassreadmeparserphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-readme-parser.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiclassbasephp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php</a></li>
<li>sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/</li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiroutesclassinternalstatsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-internal-stats.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiclassbasephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php                              (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php        2016-04-20 11:08:56 UTC (rev 2985)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,53 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\API;
+
+/**
+ * @package WordPressdotorg_Plugin_Directory
+ */
+
+class Base {
+       /**
+        * Initialises each API route we offer.
+        */
+       static function load_routes() {
+               new Routes\Internal_Stats();
+       }
+
+       /**
+        * A validation callback for REST API Requests to ensure a valid plugin slug is presented.
+        *
+        * @param string $value The plugin slug to be checked for.
+        * @return bool Whether the plugin slug exists.
+        */
+       function validate_plugin_slug_callback( $value ) {
+               return (bool) Plugin_Directory::get_plugin_post( $value );
+       }
+
+       /**
+        * A Permission Check callback which validates the request with a Bearer token.
+        *
+        * @param \WP_REST_Request $request The Rest API Request.
+        * @return bool|\WP_Error True if the token exists, WP_Error upon failure.
+        */
+       function permission_check_internal_api_bearer( $request ) {
+               $authorization_header = $request->get_header( 'authorization' );
+               $authorization_header = trim( str_ireplace( 'bearer', '', $authorization_header ) );
+
+               if (
+                       ! $authorization_header ||
+                       ! defined( 'PLUGIN_API_INTERNAL_BEARER_TOKEN' ) ||
+                       ! hash_equals( PLUGIN_API_INTERNAL_BEARER_TOKEN, $authorization_header )
+               ) {
+                       return new \WP_Error(
+                               'not_authorized',
+                               __( 'Sorry! You cannot do that.', 'wporg-plugins' ),
+                               array( 'status' => \WP_Http::UNAUTHORIZED )
+                       );
+               }
+
+               return true;
+       }
+
+}
+
+
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/class-base.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryapiroutesclassinternalstatsphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-internal-stats.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-internal-stats.php                             (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-internal-stats.php       2016-04-20 11:08:56 UTC (rev 2985)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,149 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\API\Routes;
+use WordPressdotorg\Plugin_Directory\Plugin_Directory;
+use WordPressdotorg\Plugin_Directory\API\Base;
+
+/**
+ * WordPress.org is many different systems operating with one anothers data.
+ * This endpoint offers internal w.org services a way to update stat data from
+ * these other systems from outside WordPress while triggering all WordPress actions
+ * and filters.
+ *
+ * This API is not designed for public usage, an API to fetch these statistics will also be
+ * made available.
+ *
+ * @package WordPressdotorg_Plugin_Directory
+ */
+class Internal_Stats extends Base {
+
+       function __construct() {
+               register_rest_route( 'plugins/v1', '/update-stats', array(
+                       'methods'  => \WP_REST_Server::CREATABLE,
+                       'callback' => array( $this, 'bulk_update_stats' ),
+                       'permission_callback' => array( $this, 'permission_check_internal_api_bearer' ),
+               ) );
+       }
+
+       /**
+        * Endpoint to update a whitelisted set of postmeta fields for a bunch of plugin slugs.
+        *
+        * Data is in the format of
+        * plugins: {
+        *    plugin-slug: {
+        *      active_installs: 1000
+        *    },
+        *    plugin-slug-2: {
+        *       active_instals: 1000000
+        *    }
+        * }
+        *
+        * @param \WP_REST_Request $request The Rest API Request.
+        * @return bool true
+        */
+       function bulk_update_stats( $request ) {
+               $data = $request['plugins'];
+
+               foreach ( $data as $plugin_slug => $stats ) {
+                       $plugin = Plugin_Directory::get_plugin_post( $plugin_slug );
+                       if ( ! $plugin ) {
+                               continue;
+                       }
+
+                       foreach ( $stats as $stat_name => $value ) {
+                               if ( 'active_installs' == $stat_name ) {
+                                       $value = $this->sanitize_active_installs( $value, $plugin );
+                               } elseif ( 'usage' == $stat_name ) {
+                                       $value = $this->sanitize_usage_numbers( $value, $plugin );
+                               } elseif ( 'support_threads' == $stat_name || 'support_threads_resolved' == $stat_name ) {
+                                       $value = (int) $value;
+                               } else {
+                                       continue; // Unknown key
+                               }
+
+                               update_post_meta( $plugin->ID, $stat_name, wp_slash( $value ) );
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Sanitizes the Active Install count number to a rounded display value.
+        *
+        * @param int $active_installs The raw active install number.
+        * @return int The sanitized version for display.
+        */
+       protected function sanitize_active_installs( $active_installs ) {
+               if ( $active_installs > 1000000 ) {
+                       // 1 million +;
+                       return 1000000;
+               } elseif ( $active_installs > 100000 ) {
+                       $round = 100000;
+               } elseif ( $active_installs > 10000 ) {
+                       $round = 10000;
+               } elseif ( $active_installs > 1000 ) {
+                       $round = 1000;
+               } elseif ( $active_installs > 100 ) {
+                       $round = 100;
+               } else {
+                       // Rounded to ten, else 0
+                       $round = 10;
+               }
+
+               return floor( $active_installs / $round ) * $round;
+       }
+
+       /**
+        * Sanitizes the usage figures for a plugin.
+        *
+        * Versions higher than the latest branch will be excluded.
+        * Versions which have a usage below 5% will be combined into 'other'
+        * unless it's the latest branch, or if there's only one branch which is less than 5%.
+        *
+        * @param array   $usage  An array of the branch usage numbers.
+        * @param WP_Post $plugin The plugin's WP_Post instance.
+        * @return array An array containing the percentages for the given plugin.
+        */
+       protected function sanitize_usage_numbers( $usage, $plugin ) {
+               $latest_version = get_post_meta( $plugin->ID, 'version', true );
+               $latest_branch = implode( '.', array_slice( explode('.', $latest_version ), 0, 2 ) );
+
+               // Exclude any version strings higher than the latest plugin version (ie. 99.9)
+               foreach ( $usage as $version => $count ) {
+                       if ( version_compare( $version, $latest_version, '>' ) || 0 === strlen( $version ) ) {
+                               unset( $usage[ $version ] );
+                       }
+               }
+
+               // The percentage at which we combine versions into an "other" group.
+               // Note: The latest branch will NOT fold into this.
+               $percent_cut_off = 5;
+
+               // Calculate the percentage of each version branch
+               $total = array_sum( $usage );
+               $others = array();
+               foreach ( $usage as $version => $count ) {
+                       $percent = round( $count / $total * 100, 2 );
+
+                       if ( $percent < $percent_cut_off && $version != $latest_branch ) {
+                               $others[ $version ] = $count;
+                               unset( $usage[ $version ] );
+                               continue;
+                       }
+                       $usage[ $version ] = $percent;
+               }
+
+               // If there was only one version < $percent_cut_off then display it as-is
+               if ( count( $others ) == 1 ) {
+                       $version = array_keys( $others );
+                       $version = array_shift( $version );
+                       $usage[ $version ] = round( $others[ $version ] / $total * 100, 2 );
+               // Else we'll add an 'others' version.
+               } elseif ( count( $others ) > 1 ) {
+                       $usage['other'] = round( array_sum( $others ) / $total * 100, 2 );
+               }
+
+               return $usage;
+       }
+
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/api/routes/class-internal-stats.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclassplugindirectoryphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php      2016-04-19 20:15:57 UTC (rev 2984)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php        2016-04-20 11:08:56 UTC (rev 2985)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -29,6 +29,9 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                add_filter( 'map_meta_cap', array( __NAMESPACE__ . '\Capabilities', 'map_meta_cap' ), 10, 4 );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Load the API routes
+               add_action( 'rest_api_init', array( __NAMESPACE__ . '\API\Base', 'load_routes' ) );
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Load all Admin-specific items.
</span><span class="cx" style="display: block; padding: 0 10px">                // Cannot be included on `admin_init` to allow access to menu hooks
</span><span class="cx" style="display: block; padding: 0 10px">                if ( defined( 'WP_ADMIN' ) && WP_ADMIN ) {
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryclassreadmeparserphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-readme-parser.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-readme-parser.php 2016-04-19 20:15:57 UTC (rev 2984)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-readme-parser.php   2016-04-20 11:08:56 UTC (rev 2985)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -42,15 +42,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        // These are the valid header mappings for the header
</span><span class="cx" style="display: block; padding: 0 10px">        private $valid_headers = array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        'tested'            => 'tested',
-                       'tested up to'      => 'tested',
-                       'requires'          => 'requires',
-                       'requires at least' => 'requires',
-                       'tags'              => 'tags',
-                       'contributors'      => 'contributors',
-                       'donate link'       => 'donate_link',
-                       'stable tag'        => 'stable_tag',
-               );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         'tested'            => 'tested',
+               'tested up to'      => 'tested',
+               'requires'          => 'requires',
+               'requires at least' => 'requires',
+               'tags'              => 'tags',
+               'contributors'      => 'contributors',
+               'donate link'       => 'donate_link',
+               'stable tag'        => 'stable_tag',
+       );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        public function __construct( $file ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $this->parse_readme( $file );
</span></span></pre>
</div>
</div>

</body>
</html>