<!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>[14264] sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory: Plugin Directory: Admin: Add a cronjob metabox for highly-trusted users, to ease debugging plugin imports.</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 { white-space: pre-line; 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/14264">14264</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/14264","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>2024-12-11 07:38:03 +0000 (Wed, 11 Dec 2024)</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: Admin: Add a cronjob metabox for highly-trusted users, to ease debugging plugin imports.

This allows those users to see the output from the cron jobs that are related to the plugin.

This is only shown to super admins and in non-production environments for now.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminclasscustomizationsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php</a></li>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryjobsclassmanagerphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-manager.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminmetaboxclasscronlogsphp">sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-cron-logs.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminclasscustomizationsphp"></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/admin/class-customizations.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/admin/class-customizations.php  2024-12-11 07:03:11 UTC (rev 14263)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/class-customizations.php    2024-12-11 07:38:03 UTC (rev 14264)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -776,6 +776,16 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        );
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // For highly trusted users, add a cron-jobs metabox for debugging plugin imports.
+               if ( is_super_admin() || 'production' !== wp_get_environment_type() ) {
+                       add_meta_box(
+                               'cron-logs',
+                               'Cron Job Logs',
+                               array( __NAMESPACE__ . '\Metabox\Cron_Logs', 'display' ),
+                               'plugin', 'normal', 'low'
+                       );
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 // Remove unnecessary metaboxes.
</span><span class="cx" style="display: block; padding: 0 10px">                remove_meta_box( 'commentsdiv', 'plugin', 'normal' );
</span><span class="cx" style="display: block; padding: 0 10px">                remove_meta_box( 'commentstatusdiv', 'plugin', 'normal' );
</span></span></pre></div>
<a id="sitestrunkwordpressorgpublic_htmlwpcontentpluginsplugindirectoryadminmetaboxclasscronlogsphp"></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/admin/metabox/class-cron-logs.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/admin/metabox/class-cron-logs.php                               (rev 0)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-cron-logs.php 2024-12-11 07:38:03 UTC (rev 14264)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,136 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+namespace WordPressdotorg\Plugin_Directory\Admin\Metabox;
+use WordPressdotorg\Plugin_Directory\Jobs\Manager;
+
+/**
+ * Displays logs of cron jobs for the plugin.
+ *
+ * @package WordPressdotorg\Plugin_Directory\Admin\Metabox
+ */
+class Cron_Logs {
+
+       /*
+        * Displays the cron-jobs relevant to the current plugin.
+        */
+       public static function display() {
+               $post = get_post();
+
+               $jobs = Manager::get_plugin_cron_jobs( $post, [], true );
+
+               if ( empty( $jobs ) ) {
+                       echo '<p>No cron jobs found.</p>';
+                       return;
+               }
+
+               // Reverse the logs so the most recent is at the top.
+               $jobs = array_reverse( $jobs );
+
+               echo '<table class="widefat cron-jobs">';
+               echo '<thead>
+                       <tr>
+                               <th>When</th>
+                               <th colspan="2">Task</th>
+                       </tr>
+                       </thead>';
+
+               foreach ( $jobs as $job ) {
+                       $task_name = explode( ':', $job->hook )[0];
+                       $task_name = ucwords( str_replace( '_', ' ', $task_name ) );
+
+                       $task_desc = '';
+                       // Insert certain job args into the task name.
+                       foreach (
+                               [
+                                       'revisions' => 'Revision',
+                                       'tags_touched' => 'Tags'
+                               ] as $field => $name
+                       ) {
+                               if ( ! empty( $job->args[0][ $field ] ) ) {
+                                       $task_desc .= '<span>' . $name . ': ' . ( is_array( $job->args[0][ $field ] ) ? implode( ', ', $job->args[0][ $field ] ) : $job->args[0][ $field ] ) . '</span>';
+                               }
+                       }
+
+                       $logs = [];
+                       if ( ! empty( $job->logs ) ) {
+                               foreach ( $job->logs as $log ) {
+                                       $content = '';
+                                       if ( $log->content['stderr'] ?? '' ) {
+                                               $content .= '<strong>STDERR:</strong> ' . esc_html( trim( $log->content['stderr'] ) ) . '<br>';
+                                       }
+                                       if ( $log->content['stdout'] ?? '' ) {
+                                               $content .= '<strong>STDOUT:</strong> ' . esc_html( trim( $log->content['stdout'] ) );
+                                       }
+                                       if ( ! $content ) {
+                                               $content = '<em>No output generated.</em>';
+                                       }
+
+                                       // Markup some logs to incate that it's an expected "error".
+                                       $content = preg_replace(
+                                               '/(End-of-central-directory signature not found|cannot find zipfile directory in one of)/i',
+                                               '<abbr title="This warning is expected. This is not an error.">$1</abbr>',
+                                               $content
+                                       );
+                                       // Some logs might include a path, let's remove it.
+                                       $content = str_ireplace( ABSPATH, '/', $content );
+
+                                       $logs[] = sprintf(
+                                               '<strong>Timestamp:</strong> %s finished %s after requested time<br>%s',
+                                               esc_html( $log->timestamp ),
+                                               esc_html( human_time_diff( strtotime( $log->timestamp ), $job->start ) ),
+                                               $content
+                                       );
+                               }
+                       }
+                       $logs = implode( '<br><br>', $logs );
+
+                       printf(
+                               '<tr id="job-%d" class="job">
+                                       <td title="%s">%s</td>
+                                       <td>%s<span>%s</span></td>
+                                       <td>%s</td>
+                               </tr>
+                               <tr class="log hidden">
+                                       <td colspan="3">
+                                               <pre>%s<br><strong>Job Args:</strong> %s</pre>
+                                       </td>
+                               </tr>',
+                               esc_attr( $job->id ),
+                               esc_attr( human_time_diff( $job->nextrun ?: $job->start ) . ' ago' ),
+                               date( 'Y-m-d H:i:s', $job->nextrun ?: $job->start ),
+                               $task_name,
+                               esc_html( $job->status ),
+                               $task_desc,
+                               $logs,
+                               esc_html( json_encode( $job->args[0], JSON_PRETTY_PRINT ) )
+                       );
+               }
+
+               echo '</table>';
+               echo '
+                       <style>
+                               table.cron-jobs tr.job {
+                                       cursor: pointer;
+                               }
+                               table.cron-jobs tr.job:hover,
+                               table.cron-jobs tr.job:hover + tr.log {
+                                       background-color: #f9f9f9;
+                               }
+                               table.cron-jobs tr.job span {
+                                       display: block;
+                                       font-size: 0.8em;
+                                       margin-top: 5px;
+                               }
+                               table.cron-jobs tr.log pre {
+                                       white-space: pre-wrap;
+                               }
+                       </style>
+                       <script>
+                       jQuery( document ).ready( function() {
+                               jQuery( "table.cron-jobs tr.job" ).click( function() {
+                                       jQuery( this ).next().toggleClass( "hidden" );
+                               } );
+                       } );
+               </script>';
+
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/admin/metabox/class-cron-logs.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_htmlwpcontentpluginsplugindirectoryjobsclassmanagerphp"></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/jobs/class-manager.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/jobs/class-manager.php  2024-12-11 07:03:11 UTC (rev 14263)
+++ sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/jobs/class-manager.php    2024-12-11 07:38:03 UTC (rev 14264)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -11,6 +11,20 @@
</span><span class="cx" style="display: block; padding: 0 10px"> class Manager {
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * The cron tasks that are triggered by a colon-based hook.
+        *
+        * @see Manager::register_colon_based_hook_handlers()
+        * @static
+        * @var array
+        */
+       public static $wildcard_cron_tasks = array(
+               'import_plugin'      => array( __NAMESPACE__ . '\Plugin_Import', 'cron_trigger' ),
+               'import_plugin_i18n' => array( __NAMESPACE__ . '\Plugin_i18n_Import', 'cron_trigger' ),
+               'import_zip'         => array( __NAMESPACE__ . '\Plugin_ZIP_Import', 'cron_trigger' ),
+               'tide_sync'          => array( __NAMESPACE__ . '\Tide_Sync', 'cron_trigger' ),
+       );
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * Add all the actions for cron tasks and schedules.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><span class="cx" style="display: block; padding: 0 10px">        public function __construct() {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -290,20 +304,14 @@
</span><span class="cx" style="display: block; padding: 0 10px">        public function register_colon_based_hook_handlers() {
</span><span class="cx" style="display: block; padding: 0 10px">                $cron_array = get_option( 'cron' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $wildcard_cron_tasks = array(
-                       'import_plugin'      => array( __NAMESPACE__ . '\Plugin_Import', 'cron_trigger' ),
-                       'import_plugin_i18n' => array( __NAMESPACE__ . '\Plugin_i18n_Import', 'cron_trigger' ),
-                       'tide_sync'          => array( __NAMESPACE__ . '\Tide_Sync', 'cron_trigger' ),
-               );
-
</del><span class="cx" style="display: block; padding: 0 10px">                 // Add the wildcard cron task above to the specified colon-based hook.
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $add_callback = static function( $hook ) use( $wildcard_cron_tasks ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $add_callback = static function( $hook ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                         if ( ! str_contains( $hook, ':' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                return;
</span><span class="cx" style="display: block; padding: 0 10px">                        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        list( $partial_hook, $slug ) = explode( ':', $hook, 2 );
-                       $callback                    = $wildcard_cron_tasks[ $partial_hook ] ?? false;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $partial_hook = explode( ':', $hook )[0];
+                       $callback     = self::$wildcard_cron_tasks[ $partial_hook ] ?? false;
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                        if ( ! $callback ) {
</span><span class="cx" style="display: block; padding: 0 10px">                                return;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -364,5 +372,73 @@
</span><span class="cx" style="display: block; padding: 0 10px">                Tools::clear_memory_heavy_variables();
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        /**
+        * Fetch all the cron jobs for a plugin.
+        *
+        * @static
+        *
+        * @param \WP_Post $plugin    The plugin post object.
+        * @param array    $args      Additional arguments to filter the jobs. See \HM\Cavalcade\Plugin\Job::get_jobs_by_query() for more details.
+        * @param bool     $with_logs Whether to fetch logs for the jobs.
+        * @return array
+        */
+       public static function get_plugin_cron_jobs( \WP_Post $plugin, $args = [], $with_logs = false ) {
+               global $wpdb;
+
+               if ( ! class_exists( '\HM\Cavalcade\Plugin\Job' ) ) {
+                       return [];
+               }
+
+               $args['statuses'] ??= [ 'waiting', 'running', 'completed', 'failed', 'cancelled' ];
+               $args['limit']    ??= 20;
+               $args['args']     ??= null; // All jobs, regardless of args.
+
+               $jobs = [];
+               foreach ( self::$wildcard_cron_tasks as $job_prefix => $callback ) {
+                       $args['hook'] = $job_prefix . ':' . $plugin->post_name;
+                       $jobs = array_merge(
+                               $jobs,
+                               \HM\Cavalcade\Plugin\Job::get_jobs_by_query( $args )
+                       );
+               }
+
+               // Fetch logs for the tasks.
+               if ( $with_logs ) {
+                       $log_table = str_replace( 'jobs', 'logs', \HM\Cavalcade\Plugin\Job::get_table() );
+                       foreach ( $jobs as &$job ) {
+                               // Fetch logs for the task.
+                               $job->logs = $wpdb->get_results(
+                                       $wpdb->prepare(
+                                               "SELECT status, timestamp, content FROM %i WHERE job = %d ORDER BY id DESC LIMIT 20",
+                                               $log_table,
+                                               $job->id
+                                       )
+                               );
+                               // Decode the JSON content.
+                               array_walk( $job->logs, static function( &$log ) {
+                                       $log->content = json_decode( $log->content, true ) ?: $log->content;
+                               } );
+                       }
+               }
+
+               // Sort jobs based on last run.
+               usort( $jobs, static function( $a, $b ) {
+                       $a_last_log = 0;
+                       $b_last_log = 0;
+                       if ( $a->logs ) {
+                               $a_last_log = max( array_map( 'strtotime', wp_list_pluck( $a->logs, 'timestamp' ) ) );
+                       }
+                       if ( $b->logs ) {
+                               $b_last_log = max( array_map( 'strtotime', wp_list_pluck( $b->logs, 'timestamp' ) ) );
+                       }
+
+                       $a_last_log = max( $a->start, $a_last_log );
+                       $b_last_log = max( $b->start, $b_last_log );
+
+                       return $a_last_log <=> $b_last_log;
+               } );
+
+               return $jobs;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span></span></pre>
</div>
</div>

</body>
</html>