*/ public static function all(): array { return array( 'read' => __( 'Read content, builder layouts, products, forms and settings (secrets redacted)', 'handymcp' ), 'content:write' => __( 'Create and edit posts, pages, builder layouts, media, menus and SEO', 'handymcp' ), 'commerce:write' => __( 'Create and edit WooCommerce products, prices, stock and categories', 'handymcp' ), 'forms:write' => __( 'Create and edit Contact Form 7 and WPForms forms', 'handymcp' ), ); } /** * Short name of a scope for chips and lists. * * @param string $scope Scope. */ public static function short( string $scope ): string { $names = array( 'read' => __( 'Read', 'handymcp' ), 'content:write' => __( 'Edit content', 'handymcp' ), 'commerce:write' => __( 'Manage store', 'handymcp' ), 'forms:write' => __( 'Manage forms', 'handymcp' ), ); return $names[ $scope ] ?? $scope; } /** * Keep only known scopes. * * @param array $scopes Raw scopes. * @return string[] */ public static function sanitize( array $scopes ): array { $known = array_keys( self::all() ); $out = array(); foreach ( $scopes as $s ) { $s = strtolower( trim( (string) $s ) ); if ( in_array( $s, $known, true ) ) { $out[] = $s; } } if ( ! in_array( 'read', $out, true ) && ! empty( $out ) ) { array_unshift( $out, 'read' ); } return array_values( array_unique( $out ) ); } /** * Parse a space-delimited OAuth scope string. * * @param string $scope Scope string. * @return string[] */ public static function parse( string $scope ): array { $list = preg_split( '/\s+/', trim( $scope ) ); return self::sanitize( $list ? $list : array() ); } /** Scopes that only matter when a matching plugin is installed. */ const PLUGIN_SCOPES = array( 'commerce:write', 'forms:write' ); /** * Whether a scope has anything to act on here right now. * * @param string $scope Scope. */ public static function relevant( string $scope ): bool { if ( 'commerce:write' === $scope ) { return class_exists( '\WooCommerce' ); } if ( 'forms:write' === $scope ) { return (bool) \HandyMCP\Integrations\Forms::detected(); } return true; } /** * Scopes to show on the consent screen: drop the ones for plugins that are not installed. * * @param string[] $scopes Requested scopes. * @return string[] */ public static function offer( array $scopes ): array { return array_values( array_filter( $scopes, array( __CLASS__, 'relevant' ) ) ); } /** * Scopes to grant. With "Edit content", the plugin scopes that were not shown because * their plugin is not installed are included, so the connection keeps working when * that plugin is installed later. A scope that was shown and switched off stays off. * * @param string[] $granted Scopes the person approved. * @param string[] $asked Scopes the app asked for. * @return string[] */ public static function grant( array $granted, array $asked ): array { if ( in_array( 'content:write', $granted, true ) ) { foreach ( self::PLUGIN_SCOPES as $scope ) { if ( in_array( $scope, $asked, true ) && ! self::relevant( $scope ) ) { $granted[] = $scope; } } } return self::sanitize( $granted ); } /** * Default scopes offered for new OAuth consent. * * @return string[] */ public static function default_oauth(): array { return array( 'read', 'content:write', 'commerce:write', 'forms:write' ); } }