keyring.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. /*
  3. Plugin Name: Keyring
  4. Plugin URI: http://dentedreality.com.au/projects/wp-keyring/
  5. Description: Keyring helps you manage your keys. It provides a generic, very hookable framework for connecting to remote systems and managing your access tokens, username/password combos etc for those services. On its own it doesn't do much, but it enables other plugins to do things that require authorization to act on your behalf.
  6. Version: 1.6
  7. Author: Beau Lebens
  8. Author URI: http://dentedreality.com.au
  9. License: GPL v2 or newer <https://www.gnu.org/licenses/gpl.txt>
  10. */
  11. // Define this in your wp-config (and set to true) to enable debugging
  12. defined( 'KEYRING__DEBUG_MODE' ) or define( 'KEYRING__DEBUG_MODE', false );
  13. // The name of a class which extends Keyring_Store to handle storage/manipulation of tokens.
  14. // Optionally define this in your wp-config.php or some other global config file.
  15. defined( 'KEYRING__TOKEN_STORE' ) or define( 'KEYRING__TOKEN_STORE', 'Keyring_SingleStore' );
  16. // Keyring can be run in "headless" mode, which just avoids creating any UI, and leaves
  17. // that up to you. Defaults to off (provides its own basic UI).
  18. defined( 'KEYRING__HEADLESS_MODE' ) or define( 'KEYRING__HEADLESS_MODE', false );
  19. // Debug/messaging levels. Don't mess with these
  20. define( 'KEYRING__DEBUG_NOTICE', 1 );
  21. define( 'KEYRING__DEBUG_WARN', 2 );
  22. define( 'KEYRING__DEBUG_ERROR', 3 );
  23. // Indicates Keyring is installed/active so that other plugins can detect it
  24. define( 'KEYRING__VERSION', '1.6' );
  25. /**
  26. * Core Keyring class that handles UI and the general flow of requesting access tokens etc
  27. * to manage access to remote services.
  28. *
  29. * @package Keyring
  30. */
  31. class Keyring {
  32. protected $registered_services = array();
  33. protected $store = false;
  34. protected $errors = array();
  35. protected $messages = array();
  36. protected $token_store = '';
  37. var $admin_page = 'keyring';
  38. function __construct() {
  39. if ( ! KEYRING__HEADLESS_MODE ) {
  40. require_once dirname( __FILE__ ) . '/admin-ui.php';
  41. Keyring_Admin_UI::init();
  42. add_filter( 'keyring_admin_url', function( $url, $params ) {
  43. $url = admin_url( 'tools.php?page=' . Keyring::init()->admin_page );
  44. return add_query_arg( $params, $url );
  45. }, 10, 2 );
  46. }
  47. // This is used internally to create URLs, and also to know when to
  48. // attach handers. @see admin_url() and request_handlers()
  49. $this->admin_page = apply_filters( 'keyring_admin_page', 'keyring' );
  50. }
  51. static function &init( $force_load = false ) {
  52. static $instance = false;
  53. if ( !$instance ) {
  54. if ( ! KEYRING__HEADLESS_MODE )
  55. load_plugin_textdomain( 'keyring', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
  56. $instance = new Keyring;
  57. // Keyring is being loaded 'late', so we need to do some extra set-up
  58. if ( did_action( 'init' ) || $force_load ) {
  59. $instance->plugins_loaded();
  60. do_action( 'keyring_load_services' );
  61. }
  62. } else {
  63. if ( $force_load ) {
  64. $instance->plugins_loaded();
  65. do_action( 'keyring_load_services' );
  66. }
  67. }
  68. return $instance;
  69. }
  70. static function plugins_loaded() {
  71. // Load stores early so we can confirm they're loaded correctly
  72. require_once dirname( __FILE__ ) . '/store.php';
  73. do_action( 'keyring_load_token_stores' );
  74. $keyring = Keyring::init();
  75. $keyring->token_store = apply_filters( 'keyring_token_store', defined( 'KEYRING__TOKEN_STORE' ) ? KEYRING__TOKEN_STORE : false );
  76. if ( !class_exists( $keyring->token_store ) || !in_array( 'Keyring_Store', class_parents( $keyring->token_store ) ) )
  77. wp_die( sprintf( __( 'Invalid <code>KEYRING__TOKEN_STORE</code> specified. Please make sure <code>KEYRING__TOKEN_STORE</code> is set to a valid classname for handling token storage in <code>%s</code> (or <code>wp-config.php</code>)', 'keyring' ), __FILE__ ) );
  78. // Load base token and service definitions + core services
  79. require_once dirname( __FILE__ ) . '/token.php';
  80. require_once dirname( __FILE__ ) . '/service.php'; // Triggers a load of all core + extended service definitions
  81. // Initiate Keyring
  82. add_action( 'init', array( 'Keyring', 'init' ), 1 );
  83. // Load external Services (plugins etc should hook to this to define new ones/extensions)
  84. add_action( 'init', function() {
  85. do_action( 'keyring_load_services' );
  86. }, 2 );
  87. /**
  88. * And trigger request handlers, which plugins and extended Services use to handle UI,
  89. * redirects, errors etc.
  90. * @see ::request_handlers()
  91. */
  92. add_action( 'admin_init', array( 'Keyring', 'request_handlers' ), 100 );
  93. }
  94. /**
  95. * Core request handler which is the crux of everything. An action is called
  96. * here for almost everything Keyring does, so you can use it to intercept
  97. * almost everything. Based entirely on $_REQUEST[page|action|service]
  98. */
  99. static function request_handlers() {
  100. global $current_user;
  101. if ( defined( 'KEYRING__FORCE_USER' ) && KEYRING__FORCE_USER && in_array( $_REQUEST['action'], array( 'request', 'verify' ) ) ) {
  102. global $current_user;
  103. $real_user = $current_user->ID;
  104. wp_set_current_user( KEYRING__FORCE_USER );
  105. }
  106. if (
  107. !empty( $_REQUEST['action'] )
  108. &&
  109. in_array( $_REQUEST['action'], apply_filters( 'keyring_core_actions', array( 'request', 'verify', 'created', 'delete', 'manage' ) ) )
  110. &&
  111. !empty( $_REQUEST['service'] )
  112. &&
  113. in_array( $_REQUEST['service'], array_keys( Keyring::get_registered_services() ) )
  114. ) {
  115. // We have an action here to allow us to do things pre-authorization, just in case
  116. do_action( "pre_keyring_{$_REQUEST['service']}_{$_REQUEST['action']}", $_REQUEST );
  117. // Core nonce check required for everything. "keyring-ACTION" is the kr_nonce format
  118. if ( !isset( $_REQUEST['kr_nonce'] ) || !wp_verify_nonce( $_REQUEST['kr_nonce'], 'keyring-' . $_REQUEST['action'] ) ) {
  119. Keyring::error( __( 'Invalid/missing Keyring core nonce. All core actions require a valid nonce.', 'keyring' ) );
  120. exit;
  121. }
  122. Keyring_Util::debug( "keyring_{$_REQUEST['service']}_{$_REQUEST['action']}" );
  123. Keyring_Util::debug( $_GET );
  124. do_action( "keyring_{$_REQUEST['service']}_{$_REQUEST['action']}", $_REQUEST );
  125. if ( 'delete' == $_REQUEST['action'] )
  126. do_action( "keyring_connection_deleted", $_REQUEST['service'], $_REQUEST );
  127. }
  128. if ( defined( 'KEYRING__FORCE_USER' ) && KEYRING__FORCE_USER && in_array( $_REQUEST['action'], array( 'request', 'verify' ) ) )
  129. wp_set_current_user( $real_user );
  130. }
  131. static function register_service( Keyring_Service $service ) {
  132. if ( Keyring_Util::is_service( $service ) ) {
  133. Keyring::init()->registered_services[ $service->get_name() ] = $service;
  134. return true;
  135. }
  136. return false;
  137. }
  138. static function get_registered_services() {
  139. return Keyring::init()->registered_services;
  140. }
  141. static function get_service_by_name( $name ) {
  142. $keyring = Keyring::init();
  143. if ( !isset( $keyring->registered_services[ $name ] ) )
  144. return null;
  145. return $keyring->registered_services[ $name ];
  146. }
  147. static function get_token_store() {
  148. $keyring = Keyring::init();
  149. if ( !$keyring->store )
  150. $keyring->store = call_user_func( array( $keyring->token_store, 'init' ) );
  151. return $keyring->store;
  152. }
  153. static function message( $str ) {
  154. $keyring = Keyring::init();
  155. $keyring->messages[] = $str;
  156. }
  157. /**
  158. * Generic error handler/trigger.
  159. * @param String $str Informational message (user-readable)
  160. * @param array $info Additional information relating to the error.
  161. */
  162. static function error( $str, $info = array() ) {
  163. $keyring = Keyring::init();
  164. $keyring->errors[] = $str;
  165. do_action( 'keyring_error', $str, $info, isset( $this ) ? $this : null );
  166. wp_die( $str, __( 'Keyring Error', 'keyring' ) );
  167. exit;
  168. }
  169. function has_errors() {
  170. return count( $this->errors );
  171. }
  172. function has_messages() {
  173. return count( $this->messages );
  174. }
  175. function get_messages() {
  176. return $this->messages;
  177. }
  178. function get_errors() {
  179. return $this->errors;
  180. }
  181. }
  182. class Keyring_Util {
  183. static function debug( $str, $level = KEYRING__DEBUG_NOTICE ) {
  184. if ( !KEYRING__DEBUG_MODE )
  185. return;
  186. if ( is_object( $str ) || is_array( $str ) )
  187. $str = print_r( $str, true );
  188. switch ( $level ) {
  189. case KEYRING__DEBUG_WARN :
  190. echo "<div style='border:solid 1px #000; padding: 5px; background: #eee;'>Keyring Warning: $str</div>";
  191. break;
  192. case KEYRING__DEBUG_ERROR :
  193. wp_die( '<h1>Keyring Error:</h1>' . '<p>' . $str . '</p>' );
  194. exit;
  195. }
  196. error_log( "Keyring: $str" );
  197. }
  198. static function is_service( $service ) {
  199. if ( is_object( $service ) && is_subclass_of( $service, 'Keyring_Service' ) )
  200. return true;
  201. return false;
  202. }
  203. static function has_custom_ui( $service, $action ) {
  204. return has_action( "keyring_{$service}_{$action}_ui" );
  205. }
  206. /**
  207. * Get a URL to the Keyring admin UI, works kinda like WP's admin_url()
  208. *
  209. * @param string $service Shortname of a specific service.
  210. * @return URL to Keyring admin UI (main listing, or specific service verify process)
  211. */
  212. static function admin_url( $service = false, $params = array() ) {
  213. $url = admin_url();
  214. if ( $service )
  215. $params['service'] = $service;
  216. if ( count( $params ) )
  217. $url = add_query_arg( $params, $url );
  218. return apply_filters( 'keyring_admin_url', $url, $params );
  219. }
  220. static function connect_to( $service, $for ) {
  221. Keyring_Util::debug( 'Connect to: ' . $service );
  222. // Redirect into Keyring's auth handler if a valid service is provided
  223. $kr_nonce = wp_create_nonce( 'keyring-request' );
  224. $request_nonce = wp_create_nonce( 'keyring-request-' . $service );
  225. wp_safe_redirect(
  226. Keyring_Util::admin_url(
  227. $service,
  228. array(
  229. 'action' => 'request',
  230. 'kr_nonce' => $kr_nonce,
  231. 'nonce' => $request_nonce,
  232. 'for' => $for
  233. )
  234. )
  235. );
  236. exit;
  237. }
  238. static function token_select_box( $tokens, $name, $create = false ) {
  239. ?><select name="<?php echo esc_attr( $name ); ?>" id="<?php echo esc_attr( $name ); ?>">
  240. <?php if ( $create ) : ?>
  241. <option value="new"><?php _e( 'Create a new connection&hellip;', 'keyring' ); ?></option>
  242. <?php endif; ?>
  243. <?php foreach ( (array) $tokens as $token ) : ?>
  244. <option value="<?php echo $token->get_uniq_id(); ?>"><?php echo $token->get_display(); ?></option>
  245. <?php endforeach; ?>
  246. </select><?php
  247. }
  248. static function is_error( $obj ) {
  249. return is_a( $obj, 'Keyring_Error' );
  250. }
  251. }
  252. /**
  253. * Stub implementation of an error object. May at some point get custom, but
  254. * treat it like a normal WP_Error for now.
  255. */
  256. class Keyring_Error extends WP_Error { }
  257. // This is the main hook that kicks off everything. Needs to be early so we have time to load everything.
  258. add_action( 'plugins_loaded', array( 'Keyring', 'plugins_loaded' ) );