service.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. /**
  3. * A Service is a remote site/service/system for which Keyring is capable of managing
  4. * authentication. Each Service should have a series of methods for handling the creation
  5. * of an authentication token, verifying the token and for performing authenticated
  6. * requests.
  7. *
  8. * @package Keyring
  9. */
  10. abstract class Keyring_Service {
  11. const NAME = '';
  12. const LABEL = '';
  13. protected $token = false;
  14. protected $requires_token = true;
  15. protected $store = false;
  16. private $request_response_code = '';
  17. /**
  18. * Handle the first part of getting a Token for this Service. In some cases
  19. * this may involve UI, in others it might just be a redirect.
  20. */
  21. abstract function request_token();
  22. /**
  23. * Second step/verification of a Token. This is where you can make a
  24. * test request, load meta, whatever you need to do. MUST include a call
  25. * to ::verified() at the end, if the Token is successfully verified.
  26. *
  27. * @return void
  28. * @author Beau Lebens
  29. */
  30. abstract function verify_token();
  31. /**
  32. * Make an outbound request against this Service, using the current Token
  33. * if ->requires_token() return true.
  34. *
  35. * @param string $url The URL to make the request against
  36. * @param array $params Additional parameters for the request (a la WP_HTTP)
  37. * @return String containing the body of the response on success, or Keyring_Error on any non-200 response
  38. */
  39. abstract function request( $url, array $params );
  40. /**
  41. * Get a displayable string for the passed token, for this service
  42. *
  43. * @param obj $token Keyring_Access_Token object
  44. * @return String for display, describing $token
  45. */
  46. abstract function get_display( Keyring_Access_Token $token );
  47. /**
  48. * Get an array of meta data to store with this token, based on parsing the access token
  49. * details passed back from the remote service.
  50. *
  51. * @param Mixed $token
  52. * @return Array containing keyed values to store along with this token
  53. */
  54. function build_token_meta( $token ) {
  55. return apply_filters( 'keyring_access_token_meta', array(), $this->get_name(), $token, null, $this );
  56. }
  57. function __construct() {
  58. $this->store = Keyring::get_token_store();
  59. // Default methods for handling actions, should always be defined (thus abstract, see above)
  60. add_action( 'keyring_' . $this->get_name() . '_request', array( $this, 'request_token' ) );
  61. add_action( 'keyring_' . $this->get_name() . '_verify', array( $this, 'verify_token' ) );
  62. }
  63. static function &init() {
  64. static $instance = false;
  65. if ( !$instance ) {
  66. $class = get_called_class();
  67. $services = Keyring::get_registered_services();
  68. if ( in_array( $class::NAME, array_keys( $services ) ) ) {
  69. $instance = $services[ $class::NAME ];
  70. } else {
  71. $instance = new $class;
  72. Keyring::register_service( $instance );
  73. }
  74. }
  75. return $instance;
  76. }
  77. /**
  78. * Get/set whether this Service requires a token before making requests.
  79. *
  80. * @param boolean $does_it
  81. * @return True if token is required, false if not. If called with no
  82. * param, then just returns true/false. If called with a bool,
  83. * then set requirement to true/false as specified.
  84. */
  85. function requires_token( $does_it = null ) {
  86. if ( is_null( $does_it ) )
  87. return $this->requires_token;
  88. $requires = $this->requires_token;
  89. $this->requires_token = $does_it;
  90. return $requires;
  91. }
  92. function get_name() {
  93. $c = get_called_class();
  94. if ( '' != $c::NAME )
  95. $name = $c::NAME;
  96. else
  97. $name = strtolower( $c );
  98. return $name;
  99. }
  100. function get_label() {
  101. $c = get_called_class();
  102. if ( '' != $c::LABEL )
  103. $label = $c::LABEL;
  104. else
  105. $label = $this->get_name();
  106. return $label;
  107. }
  108. function set_endpoint( $type, $url, $method = 'GET' ) {
  109. $this->{$type . '_url'} = $url;
  110. $this->{$type . '_method'} = strtoupper( $method );
  111. return true;
  112. }
  113. function get_request_response_code() {
  114. return $this->request_response_code;
  115. }
  116. function set_request_response_code( $code ) {
  117. $this->request_response_code = $code;
  118. }
  119. function basic_ui() {
  120. if ( !isset( $_REQUEST['nonce'] ) || !wp_verify_nonce( $_REQUEST['nonce'], 'keyring-manage-' . $this->get_name() ) ) {
  121. Keyring::error( __( 'Invalid/missing management nonce.', 'keyring' ) );
  122. exit;
  123. }
  124. // Common Header
  125. echo '<div class="wrap">';
  126. screen_icon( 'ms-admin' );
  127. echo '<h2>' . __( 'Keyring Service Management', 'keyring' ) . '</h2>';
  128. echo '<p><a href="' . Keyring_Util::admin_url( false, array( 'action' => 'services' ) ) . '">' . __( '&larr; Back', 'keyring' ) . '</a></p>';
  129. echo '<h3>' . sprintf( __( '%s API Credentials', 'keyring' ), esc_html( $this->get_label() ) ) . '</h3>';
  130. // Handle actually saving credentials
  131. if ( isset( $_POST['api_key'] ) && isset( $_POST['api_secret'] ) ) {
  132. // Store credentials against this service
  133. $this->update_credentials( array(
  134. 'app_id' => stripslashes( $_POST['app_id'] ),
  135. 'key' => stripslashes( $_POST['api_key'] ),
  136. 'secret' => stripslashes( $_POST['api_secret'] )
  137. ) );
  138. echo '<div class="updated"><p>' . __( 'Credentials saved.', 'keyring' ) . '</p></div>';
  139. }
  140. $app_id = $api_key = $api_secret = '';
  141. if ( $creds = $this->get_credentials() ) {
  142. $app_id = $creds['app_id'];
  143. $api_key = $creds['key'];
  144. $api_secret = $creds['secret'];
  145. }
  146. echo apply_filters( 'keyring_' . $this->get_name() . '_basic_ui_intro', '' );
  147. // Output basic form for collecting key/secret
  148. echo '<form method="post" action="">';
  149. echo '<input type="hidden" name="service" value="' . esc_attr( $this->get_name() ) . '" />';
  150. echo '<input type="hidden" name="action" value="manage" />';
  151. wp_nonce_field( 'keyring-manage', 'kr_nonce', false );
  152. wp_nonce_field( 'keyring-manage-' . $this->get_name(), 'nonce', false );
  153. echo '<table class="form-table">';
  154. echo '<tr><th scope="row">' . __( 'App ID', 'keyring' ) . '</th>';
  155. echo '<td><input type="text" name="app_id" value="' . esc_attr( $app_id ) . '" id="app_id" class="regular-text"></td></tr>';
  156. echo '<tr><th scope="row">' . __( 'API Key', 'keyring' ) . '</th>';
  157. echo '<td><input type="text" name="api_key" value="' . esc_attr( $api_key ) . '" id="api_key" class="regular-text"></td></tr>';
  158. echo '<tr><th scope="row">' . __( 'API Secret', 'keyring' ) . '</th>';
  159. echo '<td><input type="text" name="api_secret" value="' . esc_attr( $api_secret ) . '" id="api_secret" class="regular-text"></td></tr>';
  160. echo '</table>';
  161. echo '<p class="submitbox">';
  162. echo '<input type="submit" name="submit" value="' . __( 'Save Changes', 'keyring' ) . '" id="submit" class="button-primary">';
  163. echo '<a href="' . esc_url( $_SERVER['HTTP_REFERER'] ) . '" class="submitdelete" style="margin-left:2em;">' . __( 'Cancel', 'keyring' ) . '</a>';
  164. echo '</p>';
  165. echo '</form>';
  166. ?><script type="text/javascript" charset="utf-8">
  167. jQuery( document ).ready( function() {
  168. jQuery( '#app_id' ).focus();
  169. } );
  170. </script><?php
  171. echo '</div>';
  172. }
  173. /**
  174. * Return any stored credentials for this service, or false if none.
  175. *
  176. * @return Array containing credentials or false if none
  177. */
  178. function get_credentials() {
  179. // First attempt custom credentials for this service
  180. // Return null from _get_credentials() to allow falling through to the other checks below
  181. // Return false if the service requires no configuration
  182. if ( method_exists( $this, '_get_credentials' ) ) {
  183. $creds = $this->_get_credentials();
  184. if ( !is_null( $creds ) )
  185. return apply_filters( 'keyring_credentials', $creds, $this->get_name() );
  186. }
  187. // Then check for generic constants
  188. $name = $this->get_name();
  189. $name = strtoupper( preg_replace( '/[^a-zA-Z0-9]/', '', $name ) ); // Remove all non alpha-numeric chars from name
  190. if (
  191. defined( 'KEYRING__' . $name . '_ID' )
  192. &&
  193. defined( 'KEYRING__' . $name . '_KEY' )
  194. &&
  195. defined( 'KEYRING__' . $name . '_SECRET' )
  196. ) {
  197. $creds = array(
  198. 'app_id' => constant( 'KEYRING__' . $name . '_ID' ),
  199. 'key' => constant( 'KEYRING__' . $name . '_KEY' ),
  200. 'secret' => constant( 'KEYRING__' . $name . '_SECRET' ),
  201. );
  202. return apply_filters( 'keyring_credentials', $creds, $this->get_name() );
  203. }
  204. // Last check in the database for a shared store of credentials
  205. $all = apply_filters( 'keyring_credentials', get_option( 'keyring_credentials' ) );
  206. if ( !empty( $all[ $this->get_name() ] ) ) {
  207. $creds = $all[ $this->get_name() ];
  208. return apply_filters( 'keyring_credentials', $creds, $this->get_name() );
  209. }
  210. return false;
  211. }
  212. /**
  213. * Update stored credentials for this service. Accept an array and just
  214. * store it in a serialized array, keyed off the name of the service.
  215. *
  216. * @param array $credentials
  217. */
  218. function update_credentials( array $credentials ) {
  219. $all = apply_filters( 'keyring_credentials', get_option( 'keyring_credentials' ) );
  220. $all[ $this->get_name() ] = $credentials;
  221. return update_option( 'keyring_credentials', $all );
  222. }
  223. /**
  224. * If a service requires some sort of configuration before it can be used (e.g. specifying a key/secret),
  225. * then this method allows you to confirm that that configuration has taken place before attempting to
  226. * use it. You can use it to ::get_credentials() or something and make sure they look valid for example.
  227. * Return a boolean. Default just returns true, meaning "this service is configured correctly and OK to use".
  228. * @return Boolean true if service is configured correctly, false otherwise.
  229. */
  230. function is_configured() {
  231. return true;
  232. }
  233. function verified( $id, $request_token = null ) {
  234. $c = get_called_class();
  235. // If something else needs to be done, do it
  236. do_action( 'keyring_connection_verified', $c::NAME, $id, $request_token );
  237. // Back to Keyring admin, with ?service=SERVICE&created=UNIQUE_ID&kr_nonce=NONCE
  238. $kr_nonce = wp_create_nonce( 'keyring-created' );
  239. $url = apply_filters( 'keyring_verified_redirect', Keyring_Util::admin_url( $c::NAME, array( 'action' => 'created', 'id' => $id, 'kr_nonce' => $kr_nonce ) ), $c::NAME );
  240. Keyring_Util::debug( 'Verified connection, redirect to ' . $url );
  241. wp_safe_redirect( $url );
  242. exit;
  243. }
  244. function is_connected() {
  245. $c = get_called_class();
  246. return Keyring::get_token_store()->count( array( 'service' => $c::NAME ) );
  247. }
  248. function store_token( $token ) {
  249. $token->meta['_classname'] = get_called_class();
  250. $id = $this->store->insert( $token );
  251. return $id;
  252. }
  253. function set_token( Keyring_Access_Token $token ) {
  254. $this->token = $token;
  255. }
  256. /**
  257. * Just returns the currently-set token for this service
  258. * @return [type] [description]
  259. */
  260. function get_token() {
  261. return $this->token;
  262. }
  263. function get_tokens( $id = false ) {
  264. $c = get_called_class();
  265. return $this->store->get_tokens( array( 'service' => $c::NAME, 'type' => 'access' ) );
  266. }
  267. function token_select_box( $name, $create = false ) {
  268. $tokens = $this->get_tokens();
  269. return Keyring_Util::token_select_box( $tokens, $name, $create );
  270. }
  271. }
  272. // Load all packaged services in the ./includes/services/ directory by including all PHP files, first in core, then in extended
  273. // Remove a Service (prevent it from loading at all) by filtering on 'keyring_services'
  274. $keyring_services = glob( dirname( __FILE__ ) . "/includes/services/core/*.php" );
  275. $keyring_services = array_merge( $keyring_services, glob( dirname( __FILE__ ) . "/includes/services/extended/*.php" ) );
  276. $keyring_services = apply_filters( 'keyring_services', $keyring_services );
  277. foreach ( $keyring_services as $keyring_service )
  278. require $keyring_service;
  279. unset( $keyring_services, $keyring_service );