Changeset 15258
- Timestamp:
- 10/01/2026 03:53:22 PM (31 hours ago)
- Location:
- sites/trunk
- Files:
-
- 6 added
- 18 edited
-
.github/unit-tests-suites.yml (modified) (1 diff)
-
api.wordpress.org/public_html/dotorg/freescout/common.php (modified) (3 diffs)
-
api.wordpress.org/public_html/dotorg/freescout/phpunit.xml (added)
-
api.wordpress.org/public_html/dotorg/freescout/plugins-themes.php (modified) (6 diffs)
-
api.wordpress.org/public_html/dotorg/freescout/profile.php (modified) (4 diffs)
-
api.wordpress.org/public_html/dotorg/freescout/tests (added)
-
api.wordpress.org/public_html/dotorg/freescout/tests/bootstrap.php (added)
-
api.wordpress.org/public_html/dotorg/freescout/tests/class-related-user-test.php (added)
-
api.wordpress.org/public_html/dotorg/freescout/tests/stubs (added)
-
api.wordpress.org/public_html/dotorg/freescout/tests/stubs/class-wp-user.php (added)
-
freescout.wordpress.net/Modules/WPOrgSSO/Http/Controllers/SsoController.php (modified) (1 diff)
-
freescout.wordpress.net/Modules/WPOrgSSO/Http/Middleware/RequireWordPressOrgLogin.php (modified) (4 diffs)
-
freescout.wordpress.net/Modules/WPOrgSSO/Providers/WPOrgSSOServiceProvider.php (modified) (4 diffs)
-
freescout.wordpress.net/Modules/WPOrgSSO/Public/js/users.js (modified) (1 diff)
-
freescout.wordpress.net/Modules/WPOrgSSO/Services/UserSync.php (modified) (3 diffs)
-
freescout.wordpress.net/Modules/WPOrgSSO/tests/LoginTest.php (modified) (1 diff)
-
freescout.wordpress.net/Modules/WPOrgSSO/tests/SsoTestCase.php (modified) (4 diffs)
-
freescout.wordpress.net/Modules/WPOrgSSO/tests/UserSyncTest.php (modified) (2 diffs)
-
freescout.wordpress.net/Modules/WPOrgSSO/tests/UsersTest.php (modified) (2 diffs)
-
freescout.wordpress.net/Modules/WPOrgSidebar/Http/Controllers/PanelController.php (modified) (1 diff)
-
freescout.wordpress.net/Modules/WPOrgSidebar/Public/js/sidebar.js (modified) (3 diffs)
-
freescout.wordpress.net/Modules/WPOrgSidebar/Services/Client.php (modified) (3 diffs)
-
freescout.wordpress.net/Modules/WPOrgSidebar/tests/SidebarTest.php (modified) (2 diffs)
-
freescout.wordpress.net/README.md (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
sites/trunk/.github/unit-tests-suites.yml
r15218 r15258 61 61 paths: 62 62 - 'api.wordpress.org/public_html/dotorg/trac/pr/**' 63 64 freescout-api: 65 type: standalone 66 name: FreeScout API 67 working-directory: api.wordpress.org/public_html/dotorg/freescout 68 paths: 69 - 'api.wordpress.org/public_html/dotorg/freescout/**' 63 70 64 71 slack-trac: -
sites/trunk/api.wordpress.org/public_html/dotorg/freescout/common.php
r15251 r15258 145 145 * Gets the email address of the WordPress.org user a conversation is about. 146 146 * 147 * Usually that's the sender, but bounces and Slack notifications are about someone else. 147 * Usually that's the sender, but bounces and Slack notifications are about someone else. Who that is comes from what 148 * the sender wrote, so it's only used once an agent asks for it, with `related` in the payload. 148 149 * 149 150 * @param object $request Request payload. … … 151 152 */ 152 153 function get_user_email_for_email( object $request ): string { 154 $user = ! empty( $request->related ) ? get_related_user( $request ) : false; 155 if ( ! $user ) { 156 $user = get_sender_user( $request ); 157 } 158 159 return $user ? $user->user_email : (string) ( $request->sender->email ?? '' ); 160 } 161 162 /** 163 * Gets the sender's WordPress.org user, by any of their addresses. 164 * 165 * @param object $request Request payload. 166 * @return \WP_User|false 167 */ 168 function get_sender_user( object $request ): \WP_User|false { 169 $sender = $request->sender ?? null; 170 $email = (string) ( $sender->email ?? '' ); 171 $user = $email ? get_user_by( 'email', $email ) : false; 172 173 if ( ! $user && ! empty( $sender->emails ) ) { 174 $user = get_user_from_emails( array_map( 'strval', (array) $sender->emails ) ); 175 } 176 177 return $user; 178 } 179 180 /** 181 * Gets the WordPress.org user a bounce or Slack notification is about, if that's someone other than the sender. 182 * 183 * Goes by the sender's address, the subject, and the body, which the sender writes: it can name anyone. 184 * 185 * @param object $request Request payload. 186 * @return \WP_User|false 187 */ 188 function get_related_user( object $request ): \WP_User|false { 153 189 $subject = (string) ( $request->conversation->subject ?? '' ); 154 $ sender = $request->sender ?? null;155 $ email = (string) ( $sender->email ?? '');156 $user = $email ? get_user_by( 'email', $email ) :false;157 158 // If this is related to a slack user, fetch their details instead.190 $email = (string) ( $request->sender->email ?? '' ); 191 $sender = get_sender_user( $request ); 192 $user = false; 193 194 // A Slack notification about a member. 159 195 if ( 160 196 false !== stripos( $email, 'slack' ) && … … 162 198 ) { 163 199 $user = get_user_by( 'slug', $m[1] ); 164 } 165 166 // If the sender has alternative emails listed, check to see if they have a profile. 167 if ( ! $user && ! empty( $sender->emails ) ) { 168 $user = get_user_from_emails( array_map( 'strval', (array) $sender->emails ) ); 169 } 170 171 // Ignore @wordpress.org "users", unless it's literally the only match. 172 if ( $user && str_ends_with( $user->user_email, '@wordpress.org' ) ) { 173 $user = false; 174 } 175 176 // Is this is a bounce for an email that we have included the username in the subject for? 177 if ( preg_match( '#Are your plugins ready, (.+?)[?]#i', $subject, $m ) ) { 178 $user = get_user_by( 'login', $m[1] ) ?: $user; 179 } 180 181 if ( ! $user && $email && is_bounce( $request ) ) { 200 } elseif ( 201 // @wordpress.org "users" send notifications, which may bounce. 202 ( ! $sender || str_ends_with( $sender->user_email, '@wordpress.org' ) ) && 203 $email && 204 is_bounce( $request ) 205 ) { 182 206 $user = get_user_from_bounce( $request ); 183 207 } 184 208 185 return $user ? $user->user_email : $email; 209 // Neither someone at WordPress.org, nor the sender after all. 210 if ( ! $user || str_ends_with( $user->user_email, '@wordpress.org' ) || ( $sender && $sender->ID === $user->ID ) ) { 211 return false; 212 } 213 214 return $user; 186 215 } 187 216 -
sites/trunk/api.wordpress.org/public_html/dotorg/freescout/plugins-themes.php
r15251 r15258 165 165 $html = '<ul class="wporg-sidebar-items">'; 166 166 167 // Reviews are the plugins team's; other mailboxes' conversations can name any plugin. 168 $show_review = str_starts_with( $mailbox_email, 'plugins' ); 169 167 170 foreach ( $post_ids as $post_id ) { 168 171 $post = get_post( (int) $post_id ); … … 176 179 if ( 'plugin' === $type ) { 177 180 // Only a review in progress has someone on it; the assignment stays after it's done. 178 if ( $ post->assigned_reviewer && in_array( $post->post_status, array( 'new', 'pending' ), true ) ) {181 if ( $show_review && $post->assigned_reviewer && in_array( $post->post_status, array( 'new', 'pending' ), true ) ) { 179 182 $reviewer_user = get_user_by( 'id', (int) $post->assigned_reviewer ); 180 183 if ( $reviewer_user ) { … … 186 189 $last_modified = $post->last_updated ? $post->last_updated : $last_modified; 187 190 188 // Get the ZIPs attached, link to the latest for pending/new. 191 // Get the ZIPs attached, link to the latest for pending/new. Unreleased, so only for reviews. 189 192 if ( in_array( $post->post_status, array( 'new', 'pending' ), true ) ) { 190 $attachments = get_posts(193 $attachments = $show_review ? get_posts( 191 194 array( 192 195 'post_parent' => $post->ID, … … 196 199 'posts_per_page' => 1, 197 200 ) 198 ) ;201 ) : array(); 199 202 $download_link = $attachments ? (string) wp_get_attachment_url( $attachments[0]->ID ) : ''; 200 203 } … … 203 206 if ( 204 207 $download_link && 205 str_starts_with( $mailbox_email, 'plugins' )&&208 $show_review && 206 209 class_exists( '\WordPressdotorg\Plugin_Directory\API\Routes\Plugin_Review' ) 207 210 ) { … … 223 226 $status = ucwords( $post->post_status ); 224 227 // This is not perfect, but close enough. 225 if ( $ post->_close_reason ) {228 if ( $show_review && $post->_close_reason ) { 226 229 $status .= ': ' . ucwords( str_replace( '-', ' ', $post->_close_reason ) ); 227 230 } -
sites/trunk/api.wordpress.org/public_html/dotorg/freescout/profile.php
r15251 r15258 28 28 $sender_email = (string) ( $request->sender->email ?? '' ); 29 29 $email = get_user_email_for_email( $request ); 30 $related = ! empty( $request->related ); 31 $slack_email = preg_match( '/(\S+@chat.wordpress.org)/i', (string) ( $request->conversation->subject ?? '' ), $m ) ? $m[1] : ''; 30 32 31 33 if ( $email ) { … … 71 73 } 72 74 73 // If this is related to a slack user, include the details of the slack account .74 if ( $user || preg_match( '/(\S+@chat.wordpress.org)/i', (string) ( $request->conversation->subject ?? '' ), $m) ) {75 // If this is related to a slack user, include the details of the slack account; one the subject names only on request. 76 if ( $user || ( $related && $slack_email ) ) { 75 77 // Someone can have several Slack accounts over the years; active ones first. 76 78 if ( $user ) { … … 80 82 $wpdb->prepare( 81 83 'SELECT * FROM slack_users WHERE profiledata LIKE %s ORDER BY deactivated ASC', 82 '%' . $wpdb->esc_like( '"email":"' . $ m[1]. '"' ) . '%'84 '%' . $wpdb->esc_like( '"email":"' . $slack_email . '"' ) . '%' 83 85 ) 84 86 ); … … 88 90 } 89 91 90 return $html; 92 // The sender wrote whatever names someone else, so an agent decides whether it's worth a look; WPOrgSidebar asks. 93 if ( $related ) { 94 $notice = '<p class="wporg-sidebar-meta">Showing the account this is about, not the sender’s. <a href="#" class="wporg-sidebar-show-sender">Show the sender</a></p>'; 95 } elseif ( get_related_user( $request ) || ( ! $user && $slack_email ) ) { 96 $notice = '<p class="wporg-sidebar-meta">This may be about someone else’s account, like a bounce. <a href="#" class="wporg-sidebar-show-related">Show it</a></p>'; 97 } else { 98 $notice = ''; 99 } 100 101 return $notice . $html; 91 102 } 92 103 -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/Http/Controllers/SsoController.php
r15246 r15258 183 183 $details = $wporg_user->to_array(); 184 184 185 // Private on WordPress.org ; the form fills itin when the user is created.185 // Private on WordPress.org, and any account can be looked up; the form fills the email in when the user is created. 186 186 if ( ! $request->user()->isAdmin() ) { 187 unset( $details['email'] );187 unset( $details['email'], $details['two_factor'], $details['blocked'] ); 188 188 } 189 189 -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/Http/Middleware/RequireWordPressOrgLogin.php
r15252 r15258 36 36 * @var int 37 37 */ 38 private const RECHECK_SECONDS = 3600; 38 private const RECHECK_SECONDS = 3600; // 1 hour. 39 40 /** 41 * How long sessions from before WordPress.org was enforced go on, in seconds. 42 * 43 * @var int 44 */ 45 private const CUTOVER_SECONDS = 86400; // 1 day. 39 46 40 47 /** … … 171 178 */ 172 179 private static function enforce_login( Request $request, ?User $user, string $action ): ?Response { 173 if ( $user instanceof User && ! self::may_stay_logged_in( $user, $request ) ) { 180 // On the first enforced request, before any session can be from while this module was off. 181 $enforced_since = WPOrgSSOServiceProvider::enforced_since(); 182 183 if ( $user instanceof User && ! self::may_stay_logged_in( $user, $request, $enforced_since ) ) { 174 184 \Auth::logout(); 175 185 $request->session()->invalidate(); … … 226 236 * Whether a logged-in user may stay logged in. 227 237 * 228 * @param User $user Logged-in user. 229 * @param Request $request Request. 238 * @param User $user Logged-in user. 239 * @param Request $request Request. 240 * @param int $enforced_since When WordPress.org was first enforced, as a Unix timestamp. 230 241 * @return bool 231 242 */ 232 private static function may_stay_logged_in( User $user, Request $request ): bool {243 private static function may_stay_logged_in( User $user, Request $request, int $enforced_since ): bool { 233 244 $session = $request->session(); 234 245 $username = (string) $session->get( WPOrgSSOServiceProvider::SESSION_USERNAME, '' ); … … 249 260 250 261 /* 251 * From before WordPress.org was enforced: going on keeps whoever switched it on logged in, so they can connect252 * the accounts. Manage » System » Tools logs everyone out, for a clean switch.262 * From before WordPress.org was enforced: going on for a day keeps whoever switched it on logged in, so they can 263 * connect the accounts. Later, an unmarked session is from while this module was off, and can't be trusted. 253 264 */ 254 return t rue;265 return time() - $enforced_since < self::CUTOVER_SECONDS; 255 266 } 256 267 -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/Providers/WPOrgSSOServiceProvider.php
r15252 r15258 94 94 95 95 /** 96 * Option of when WordPress.org was first enforced, as a Unix timestamp. 97 * 98 * @var string 99 */ 100 public const OPTION_ENFORCED_SINCE = 'wporgsso.enforced_since'; 101 102 /** 96 103 * Registers the module's dependencies. 97 104 * … … 99 106 */ 100 107 public function register(): void { 101 require_once __DIR__ . '/../vendor/autoload.php'; 108 $autoload = __DIR__ . '/../vendor/autoload.php'; 109 110 // Core switches off a module with a missing file, and WordPress.org logins with it; this keeps them enforced. 111 if ( ! is_readable( $autoload ) ) { 112 \Log::critical( '[WPOrgSSO] vendor/autoload.php is missing; WordPress.org logins fail until it is restored.' ); 113 114 return; 115 } 116 117 require_once $autoload; 102 118 } 103 119 … … 148 164 149 165 return true; 166 } 167 168 /** 169 * When WordPress.org was first enforced; the first request that enforces it records it. 170 * 171 * It stays when enforcement is switched off, so sessions from while it was off end when it's back. 172 * 173 * @return int Unix timestamp. 174 */ 175 public static function enforced_since(): int { 176 // It never changes once set; a cleared cache costs one query, and the option keeps the time. 177 return (int) \Cache::rememberForever( 178 self::OPTION_ENFORCED_SINCE, 179 static function (): int { 180 // Not core's option cache, which outlives a request in tests: a stale default would move the cutover. 181 $since = (int) \Option::get( self::OPTION_ENFORCED_SINCE, 0, true, false ); 182 if ( ! $since ) { 183 $since = time(); 184 \Option::set( self::OPTION_ENFORCED_SINCE, $since ); 185 } 186 187 return $since; 188 } 189 ); 150 190 } 151 191 … … 288 328 * Only an administrator's password login in break-glass mode may stay; the middleware ends the session of any 289 329 * other login without WordPress.org, like one from a reset link or a remember-me cookie, on its next request. 290 * Sessions from before WordPress.org was enforced go on , so whoever switches it on isn't logged out.330 * Sessions from before WordPress.org was enforced go on for a day, so whoever switches it on isn't logged out. 291 331 */ 292 332 \Event::listen( -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/Public/js/users.js
r15246 r15258 147 147 notes.push( strings.blocked ); 148 148 } 149 if ( ! user.two_factor ) { 149 // Only administrators get the account's status, too. 150 if ( false === user.two_factor ) { 150 151 notes.push( strings.no_two_factor ); 151 152 } -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/Services/UserSync.php
r15246 r15258 19 19 */ 20 20 final class UserSync { 21 22 /** 23 * Hosts avatars are downloaded from; account.php's avatars are Gravatar's. 24 * 25 * @var string[] 26 */ 27 private const AVATAR_HOSTS = array( 'gravatar.com', 'www.gravatar.com', 'secure.gravatar.com', '0.gravatar.com', '1.gravatar.com', '2.gravatar.com' ); 21 28 22 29 /** … … 62 69 * @return void 63 70 * 64 * @throws RuntimeException If the avatar can't be downloaded or saved.71 * @throws RuntimeException If the avatar isn't Gravatar's, or can't be downloaded or saved. 65 72 */ 66 73 public static function sync_avatar( User $user, string $avatar_url, Client $client ): void { … … 68 75 if ( ! $account ) { 69 76 return; 77 } 78 79 // The app server fetches it, so it must not reach anything internal. 80 if ( 'https' !== parse_url( $avatar_url, PHP_URL_SCHEME ) || ! in_array( parse_url( $avatar_url, PHP_URL_HOST ), self::AVATAR_HOSTS, true ) ) { 81 throw new RuntimeException( 'Not a Gravatar URL: ' . $avatar_url ); 70 82 } 71 83 -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/tests/LoginTest.php
r15252 r15258 309 309 310 310 /** 311 * A day after WordPress.org was enforced, a session without its marks is from while the module was off, and ends. 312 * 313 * @return void 314 */ 315 public function test_logs_out_unmarked_sessions_after_the_cutover(): void { 316 \Option::set( WPOrgSSOServiceProvider::OPTION_ENFORCED_SINCE, time() - 86400 ); 317 318 $this->actingAs( $this->user )->get( route( 'dashboard' ) )->assertRedirect( route( 'login' ) ); 319 $this->assertGuest(); 320 } 321 322 /** 311 323 * A login without WordPress.org once it's enforced ends on the next request. 312 324 * -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/tests/SsoTestCase.php
r15246 r15258 83 83 ); 84 84 85 $this->avatars = array( 'https:// avatars.test/rita.png' => self::image( 255, 0, 0 ) );85 $this->avatars = array( 'https://secure.gravatar.com/avatar/rita?s=256&d=mm' => self::image( 255, 0, 0 ) ); 86 86 87 87 $this->accounts = array( … … 92 92 'last_name' => 'Reviewer', 93 93 'email' => 'rita@example.org', 94 'avatar_url' => 'https:// avatars.test/rita.png',94 'avatar_url' => 'https://secure.gravatar.com/avatar/rita?s=256&d=mm', 95 95 'two_factor' => true, 96 96 'blocked' => false, … … 110 110 111 111 /** 112 * Answers requests to account.php and the avatar host.112 * Answers requests to account.php and for avatars. 113 113 * 114 114 * @param \Psr\Http\Message\RequestInterface $request Request. … … 116 116 */ 117 117 public function answer_api( \Psr\Http\Message\RequestInterface $request ): \GuzzleHttp\Promise\PromiseInterface { 118 if ( 'avatars.test' === $request->getUri()->getHost() ) {119 $avatar = $this->avatars[ (string) $request->getUri() ] ?? null;118 $avatar = $this->avatars[ (string) $request->getUri() ] ?? null; 119 if ( $avatar || 'secure.gravatar.com' === $request->getUri()->getHost() ) { 120 120 $response = $avatar ? new Response( 200, array( 'Content-Type' => 'image/png' ), $avatar ) : new Response( 404 ); 121 121 -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/tests/UserSyncTest.php
r15246 r15258 95 95 $hash = Account::for_user( (int) $this->user->id )->avatar_hash; 96 96 97 $this->avatars['https:// avatars.test/rita.png'] = self::image( 0, 0, 255 );97 $this->avatars['https://secure.gravatar.com/avatar/rita?s=256&d=mm'] = self::image( 0, 0, 255 ); 98 98 $this->sync(); 99 99 … … 139 139 */ 140 140 public function test_unreadable_avatar_is_logged_not_thrown(): void { 141 $this->avatars['https://avatars.test/rita.png'] = 'not an image'; 141 $this->avatars['https://secure.gravatar.com/avatar/rita?s=256&d=mm'] = 'not an image'; 142 143 $this->sync(); 144 145 $this->assertSame( 'Rita', $this->user->refresh()->first_name ); 146 $this->assertEmpty( $this->user->photo_url ); 147 } 148 149 /** 150 * Only Gravatar's avatars are downloaded, as the app server fetches them. 151 * 152 * @return void 153 */ 154 public function test_ignores_avatar_off_gravatar(): void { 155 $this->accounts['rita']['avatar_url'] = 'https://169.254.169.254/avatar.png'; 156 $this->avatars['https://169.254.169.254/avatar.png'] = self::image( 0, 255, 0 ); 142 157 143 158 $this->sync(); -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSSO/tests/UsersTest.php
r15246 r15258 139 139 140 140 /** 141 * Agents who may manage users look up accounts too, but only administrators see the private email address.142 * 143 * @return void 144 */ 145 public function test_lookup_hides_email_ from_non_administrators(): void {141 * Agents who may manage users look up accounts too, but only administrators see the email address and status. 142 * 143 * @return void 144 */ 145 public function test_lookup_hides_email_and_status_from_non_administrators(): void { 146 146 $manager = $this->create_user( User::ROLE_USER ); 147 147 $manager->permissions = array( User::PERM_EDIT_USERS => true ); … … 153 153 $this->assertSame( 'rita', $data['user']['username'] ); 154 154 $this->assertArrayNotHasKey( 'email', $data['user'] ); 155 $this->assertArrayNotHasKey( 'two_factor', $data['user'] ); 156 $this->assertArrayNotHasKey( 'blocked', $data['user'] ); 155 157 } 156 158 -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSidebar/Http/Controllers/PanelController.php
r15251 r15258 54 54 } 55 55 56 $payload = ConversationPayload::build( $conversation ); 57 58 // The account a bounce or Slack notification names, instead of the sender's, once the agent asks for it. 59 if ( request()->query( 'related' ) ) { 60 $payload['related'] = true; 61 } 62 56 63 try { 57 $response = Client::from_config( self::TIMEOUT )->post( 58 (string) $panels[ $panel ]['endpoint'], 59 ConversationPayload::build( $conversation ) 60 ); 64 $response = Client::from_config( self::TIMEOUT )->post( (string) $panels[ $panel ]['endpoint'], $payload ); 61 65 } catch ( \Throwable $e ) { 62 66 \Log::error( '[WPOrgSidebar] Could not load panel ' . $panel . ': ' . $e->getMessage() ); -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSidebar/Public/js/sidebar.js
r15251 r15258 34 34 $( '.wporg-sidebar' ).on( 'shown.bs.collapse', fitLayout ); 35 35 36 $( '.wporg-sidebar-panel[data-url]' ).each( function () { 37 const $panel = $( this ); 36 const $panels = $( '.wporg-sidebar-panel[data-url]' ); 38 37 39 $.getJSON( $panel.data( 'url' ) ) 38 $panels.each( function () { 39 loadPanel( $( this ), false ); 40 } ); 41 42 // Bounces and Slack notifications name someone else's account, which the profile panel offers to switch to, and back. 43 $( '.wporg-sidebar' ).on( 44 'click', 45 '.wporg-sidebar-show-related, .wporg-sidebar-show-sender', 46 function ( event ) { 47 const related = $( this ).hasClass( 48 'wporg-sidebar-show-related' 49 ); 50 51 event.preventDefault(); 52 $panels.each( function () { 53 loadPanel( $( this ), related ); 54 } ); 55 } 56 ); 57 58 /** 59 * Loads a panel, hiding it if it has nothing to show. 60 * 61 * @param {jQuery} $panel Panel. 62 * @param {boolean} related Whether it's about the account a bounce or Slack notification names, not the sender's. 63 */ 64 function loadPanel( $panel, related ) { 65 const $block = $panel.closest( '.conv-sidebar-block' ); 66 const previous = $panel.data( 'request' ); 67 68 // A late answer to an earlier load would show the other person. 69 if ( previous ) { 70 previous.abort(); 71 } 72 73 const request = $.getJSON( 74 $panel.data( 'url' ), 75 related ? { related: 1 } : {} 76 ); 77 $panel.data( 'request', request ); 78 79 request 40 80 .done( function ( response ) { 41 81 if ( response && response.html ) { … … 43 83 $panel.html( response.html ); 44 84 shortenLists( $panel ); 45 fitLayout();85 $block.show(); 46 86 } else { 47 $panel.closest( '.conv-sidebar-block' ).remove(); 87 // Not removed: the other account may have something to show. 88 $block.hide(); 48 89 } 90 fitLayout(); 49 91 } ) 50 .fail( function () { 92 .fail( function ( xhr, textStatus ) { 93 if ( 'abort' === textStatus ) { 94 return; 95 } 96 51 97 $panel.html( 52 98 $( '<p class="wporg-sidebar-empty">' ).text( … … 54 100 ) 55 101 ); 102 $block.show(); 56 103 } ); 57 } );104 } 58 105 59 106 /** -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSidebar/Services/Client.php
r15239 r15258 48 48 49 49 /** 50 * Guzzle handler, for tests to answer requests. 51 * 52 * @var callable|null 53 */ 54 private $handler; 55 56 /** 50 57 * Constructor. 51 58 * 52 * @param string $base_url Base URL of the endpoints. 53 * @param string $secret Shared signing secret. 54 * @param int $timeout Request timeout in seconds. 59 * @param string $base_url Base URL of the endpoints. 60 * @param string $secret Shared signing secret. 61 * @param int $timeout Request timeout in seconds. 62 * @param callable|null $handler Guzzle handler; the default sends real requests. 55 63 */ 56 public function __construct( string $base_url, string $secret, int $timeout = 10 ) {64 public function __construct( string $base_url, string $secret, int $timeout = 10, ?callable $handler = null ) { 57 65 $this->base_url = rtrim( $base_url, '/' ) . '/'; 58 66 $this->secret = $secret; 59 67 $this->timeout = $timeout; 68 $this->handler = $handler; 60 69 } 61 70 62 71 /** 63 * Creates a client from the module configuration .72 * Creates a client from the module configuration, or returns the one tests bound. 64 73 * 65 74 * @param int $timeout Request timeout in seconds. … … 67 76 */ 68 77 public static function from_config( int $timeout = 10 ): self { 78 if ( app()->bound( self::class ) ) { 79 return app( self::class ); 80 } 81 69 82 return new self( 70 83 (string) config( 'wporgsidebar.api_url' ), … … 125 138 126 139 try { 127 $http = new \GuzzleHttp\Client( \Helper::setGuzzleDefaultOptions( array( 'timeout' => $this->timeout ) ) ); 140 $options = array( 'timeout' => $this->timeout ); 141 if ( $this->handler ) { 142 $options['handler'] = \GuzzleHttp\HandlerStack::create( $this->handler ); 143 } 144 145 $http = new \GuzzleHttp\Client( \Helper::setGuzzleDefaultOptions( $options ) ); 128 146 $response = $http->post( 129 147 $this->base_url . ltrim( $endpoint, '/' ), -
sites/trunk/freescout.wordpress.net/Modules/WPOrgSidebar/tests/SidebarTest.php
r15239 r15258 12 12 use App\Conversation; 13 13 use App\User; 14 use GuzzleHttp\Handler\MockHandler; 15 use GuzzleHttp\Promise\PromiseInterface; 16 use GuzzleHttp\Psr7\Response; 14 17 use Modules\WPOrgSidebar\Providers\WPOrgSidebarServiceProvider; 18 use Modules\WPOrgSidebar\Services\Client; 19 use Psr\Http\Message\RequestInterface; 15 20 use WordPressdotorg\FreeScout\Tests\TestCase; 16 21 … … 105 110 ->assertExactJson( array( 'html' => '' ) ); 106 111 } 112 113 /** 114 * The account a bounce or Slack notification names is only asked for when the agent asks for it. 115 * 116 * @return void 117 */ 118 public function test_asks_for_the_related_account_on_request(): void { 119 $payloads = array(); 120 $this->app->instance( 121 Client::class, 122 new Client( 123 'https://api.wordpress.test/', 124 'test-secret', 125 5, 126 static function ( RequestInterface $request ) use ( &$payloads ): PromiseInterface { 127 $payloads[] = json_decode( (string) $request->getBody(), true ); 128 129 return ( new MockHandler( array( new Response( 200, array(), '{"html":"<p>Panel</p>"}' ) ) ) )( $request, array() ); 130 } 131 ) 132 ); 133 $user = $this->create_user(); 134 135 $this->actingAs( $user )->get( '/wporgsidebar/' . $this->conversation->id . '/profile' )->assertStatus( 200 ); 136 $this->actingAs( $user )->get( '/wporgsidebar/' . $this->conversation->id . '/profile?related=1' )->assertStatus( 200 ); 137 138 $this->assertArrayNotHasKey( 'related', $payloads[0] ); 139 $this->assertTrue( $payloads[1]['related'] ); 140 } 107 141 } -
sites/trunk/freescout.wordpress.net/README.md
r15252 r15258 40 40 |---|---| 41 41 | `WPORG_API_SECRET` | Shared secret; must match `FREESCOUT_SECRET` on api.wordpress.org. | 42 | `WPORG_SSO_IDP_CERT` | The identity provider's signing certificate, on one line, with or without the BEGIN/END lines. Until it and `WPORG_API_SECRET` are set, logins stay as they are, so users can be connected first. Once they are, new logins go through WordPress.org, and sessions from before go on ; Manage » System » Tools » Logout Users ends them. |42 | `WPORG_SSO_IDP_CERT` | The identity provider's signing certificate, on one line, with or without the BEGIN/END lines. Until it and `WPORG_API_SECRET` are set, logins stay as they are, so users can be connected first. Once they are, new logins go through WordPress.org, and sessions from before go on for a day; Manage » System » Tools » Logout Users ends them sooner. After that day, switching enforcement off and on again ends every session from while it was off, including a password login of whoever switches it back on: have break-glass or a connected account ready. | 43 43 44 44 Optional:
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)