keyring.php 10 KB

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