Updated Auth library with settings, added Facebook settings
Showing
6 changed files
with
1025 additions
and
2 deletions
| ... | @@ -2,7 +2,9 @@ | ... | @@ -2,7 +2,9 @@ |
| 2 | 2 | ||
| 3 | namespace Tz\WordPress\Tools\Auth; | 3 | namespace Tz\WordPress\Tools\Auth; |
| 4 | 4 | ||
| 5 | use \Exception, \LogicException, \InvalidArgumentException, \BadMethodCallException; | 5 | use Tz\WordPress\Tools; |
| 6 | use Tz\Common; | ||
| 7 | use Exception, LogicException, InvalidArgumentException, BadMethodCallException; | ||
| 6 | 8 | ||
| 7 | const REG_METH_AUTO_REG = 1; | 9 | const REG_METH_AUTO_REG = 1; |
| 8 | const REG_METH_VALID_EMAIL = 2; | 10 | const REG_METH_VALID_EMAIL = 2; |
| ... | @@ -12,6 +14,24 @@ const FORGOT_METH_RAND_PASS = 2; | ... | @@ -12,6 +14,24 @@ const FORGOT_METH_RAND_PASS = 2; |
| 12 | 14 | ||
| 13 | const ACTION_ACTIVATE = 'activate_account'; | 15 | const ACTION_ACTIVATE = 'activate_account'; |
| 14 | 16 | ||
| 17 | const OPTION_NAME = 'tz_auth'; // Database lookup key (`wp_options`.`option_name`) | ||
| 18 | |||
| 19 | call_user_func(function() { | ||
| 20 | Vars::$options = new Tools\WP_Option(OPTION_NAME); | ||
| 21 | |||
| 22 | if (is_admin()) { | ||
| 23 | require_once(__DIR__ . DIRECTORY_SEPARATOR . 'Settings.php'); | ||
| 24 | } | ||
| 25 | |||
| 26 | if (is_array(Vars::$options['third_party'])) { | ||
| 27 | foreach (Vars::$options['third_party'] as $tp => $on) { | ||
| 28 | if ($on) { | ||
| 29 | require_once(__DIR__ . DIRECTORY_SEPARATOR . $tp . DIRECTORY_SEPARATOR . $tp . '.php'); | ||
| 30 | } | ||
| 31 | } | ||
| 32 | } | ||
| 33 | }); | ||
| 34 | |||
| 15 | /** | 35 | /** |
| 16 | * Attempts to login the user | 36 | * Attempts to login the user |
| 17 | * @param {String} $username | 37 | * @param {String} $username |
| ... | @@ -80,7 +100,11 @@ function forgot_password($username, $forgot_method) { | ... | @@ -80,7 +100,11 @@ function forgot_password($username, $forgot_method) { |
| 80 | 100 | ||
| 81 | } | 101 | } |
| 82 | 102 | ||
| 83 | class Validation extends \Tz\Validation { | 103 | class Vars { |
| 104 | public static $options; | ||
| 105 | } | ||
| 106 | |||
| 107 | class Validation extends Common\Validation { | ||
| 84 | /** | 108 | /** |
| 85 | * @rule Not blank | 109 | * @rule Not blank |
| 86 | * @rule Valid WordPress username | 110 | * @rule Valid WordPress username | ... | ... |
com/Auth/Facebook/Facebook.php
0 → 100644
| 1 | <?php | ||
| 2 | /** | ||
| 3 | * This needs to go in the <html tag | ||
| 4 | * xmlns:fb="http://www.facebook.com/2008/fbml" | ||
| 5 | * | ||
| 6 | * http://wpdev.tenzinghost.com | ||
| 7 | * API Key: 83f54e078b9aa0e303bba959dc0a566f | ||
| 8 | * App Secret: e542aca35ab698121fa5917211013a41 | ||
| 9 | * App ID: 105917066126941 | ||
| 10 | * | ||
| 11 | * http://wp.cb | ||
| 12 | * API Key: 3bcccfd8c28c52197141266d9e417649 | ||
| 13 | * App Secret: 9bfcd828bc6ccef12336dea57df93ecb | ||
| 14 | * App ID: 138943536118944 | ||
| 15 | */ | ||
| 16 | |||
| 17 | namespace Tz\WordPress\Tools\Auth\Facebook; | ||
| 18 | |||
| 19 | use Tz\WordPress\Tools; | ||
| 20 | use Tz\WordPress\Tools\ShortCodes as SC; | ||
| 21 | |||
| 22 | use InvalidArgumentException; | ||
| 23 | |||
| 24 | const OPTION_NAME = 'tz_auth_fb'; | ||
| 25 | |||
| 26 | call_user_func(function() { | ||
| 27 | SC\add_shortcodes(__NAMESPACE__ . '\ShortCodes'); | ||
| 28 | Vars::$options = new Tools\WP_Option(OPTION_NAME); | ||
| 29 | |||
| 30 | if (is_admin()) { | ||
| 31 | require_once(__DIR__ . DIRECTORY_SEPARATOR . 'Settings.php'); | ||
| 32 | } | ||
| 33 | }); | ||
| 34 | |||
| 35 | function loadJSSDK() { | ||
| 36 | return ' | ||
| 37 | <div id="fb-root"></div> | ||
| 38 | <script> | ||
| 39 | window.fbAsyncInit = function() { | ||
| 40 | FB.init({appId: \'' . Vars::$options['application_id'] . '\', status: true, cookie: true, xfbml: true}); | ||
| 41 | FB.Event.subscribe(\'auth.login\', function(response) { window.location.reload(); }); | ||
| 42 | }; | ||
| 43 | (function() { | ||
| 44 | var e = document.createElement(\'script\'); e.async = true; | ||
| 45 | e.src = document.location.protocol + \'//connect.facebook.net/en_US/all.js\'; | ||
| 46 | document.getElementById(\'fb-root\').appendChild(e); | ||
| 47 | }()); | ||
| 48 | </script> | ||
| 49 | '; | ||
| 50 | } | ||
| 51 | |||
| 52 | function drawLoginButton($value = 'Login') { | ||
| 53 | echo '<fb:login-button></fb:login-button>'; | ||
| 54 | // echo '<a class="fb_button fb_button_medium"><span class="fb_button_text">' . $value . '</span></a>'; | ||
| 55 | } | ||
| 56 | |||
| 57 | class Vars { | ||
| 58 | public static $options = false; | ||
| 59 | } | ||
| 60 | |||
| 61 | class ShortCodes { | ||
| 62 | public static function fb_login_button() { | ||
| 63 | ob_start(); | ||
| 64 | drawLoginButton(); | ||
| 65 | $btn = ob_get_contents(); | ||
| 66 | ob_end_clean(); | ||
| 67 | |||
| 68 | return $btn; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | ?> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
com/Auth/Facebook/Settings.php
0 → 100644
| 1 | <?php | ||
| 2 | |||
| 3 | // todo: move ext_perms to a constant somewhere | ||
| 4 | |||
| 5 | namespace Tz\WordPress\Tools\Auth\Facebook\Settings; | ||
| 6 | |||
| 7 | use Tz\WordPress\Tools, Tz\WordPress\Tools\Auth, Tz\WordPress\Tools\Auth\Facebook; | ||
| 8 | |||
| 9 | const OPTION_SECTION_CRED = 'fb_creds'; | ||
| 10 | const OPTION_SECTION_SEL = 'fb_opts'; | ||
| 11 | |||
| 12 | call_user_func(function() { | ||
| 13 | Vars::$options = new Tools\WP_Option(Facebook\OPTION_NAME); | ||
| 14 | Tools\add_actions(__NAMESPACE__ . '\Actions'); | ||
| 15 | }); | ||
| 16 | |||
| 17 | function validate($data) { | ||
| 18 | return (array)$data; | ||
| 19 | } | ||
| 20 | |||
| 21 | class Vars { | ||
| 22 | public static $options = false; | ||
| 23 | public static $data_permissions = Array('email', 'read_insights', 'read_stream', 'read_mailbox', 'ads_management', 'xmpp_login', 'user_about_me', 'user_activities', 'user_birthday', 'user_education_history', 'user_events', 'user_groups', 'user_hometown', 'user_interests', 'user_likes', 'user_location', 'user_notes', 'user_online_presence', 'user_photo_video_tags', 'user_photos', 'user_relationships', 'user_religion_politics', 'user_status', 'user_videos', 'user_website', 'user_work_history', 'read_friendlists', 'read_requests'); | ||
| 24 | } | ||
| 25 | |||
| 26 | class Actions { | ||
| 27 | public static function admin_init() { | ||
| 28 | register_setting(Auth\Settings\OPTION_GROUP, Facebook\OPTION_NAME, __NAMESPACE__ . '\validate'); | ||
| 29 | |||
| 30 | add_settings_section(OPTION_SECTION_CRED, 'Facebook Credentials', function(){}, Auth\Settings\ADMIN_PAGE); | ||
| 31 | Tools\add_settings_fields(__NAMESPACE__ . '\Cred_Fields', Auth\Settings\ADMIN_PAGE, OPTION_SECTION_CRED); | ||
| 32 | |||
| 33 | add_settings_section(OPTION_SECTION_SEL, 'Facebook Extended Data Permissions', function() { echo '<p>Select which additional data you wish to collect from the user</p>'; }, Auth\Settings\ADMIN_PAGE); | ||
| 34 | foreach (Vars::$data_permissions as $option) { | ||
| 35 | add_settings_field($option, ucwords(str_replace('_', ' ', $option)), Array(new Opt_Fields(), $option), Auth\Settings\ADMIN_PAGE, OPTION_SECTION_SEL); | ||
| 36 | } | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | class Cred_Fields { | ||
| 41 | public static function api_key() { | ||
| 42 | echo '<input type="text" id="' . __FUNCTION__ . '" name="' . Facebook\OPTION_NAME . '[' . __FUNCTION__ . ']" value="' . Vars::$options[__FUNCTION__] . '" size="45" />'; | ||
| 43 | } | ||
| 44 | |||
| 45 | public static function application_secret() { | ||
| 46 | echo '<input type="text" id="' . __FUNCTION__ . '" name="' . Facebook\OPTION_NAME . '[' . __FUNCTION__ . ']" value="' . Vars::$options[__FUNCTION__] . '" size="45" />'; | ||
| 47 | } | ||
| 48 | |||
| 49 | public static function application_id() { | ||
| 50 | echo '<input type="text" id="' . __FUNCTION__ . '" name="' . Facebook\OPTION_NAME . '[' . __FUNCTION__ . ']" value="' . Vars::$options[__FUNCTION__] . '" />'; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | class Opt_Fields { | ||
| 55 | public function __call($fn, $args) { | ||
| 56 | echo '<input type="checkbox" id="' . $fn . '" name="' . Facebook\OPTION_NAME . '[ext_perms][' . $fn . ']" value="1" ' . checked('1', Vars::$options['ext_perms'][$fn], false) . ' />'; | ||
| 57 | } | ||
| 58 | } | ||
| 59 | ?> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
com/Auth/Facebook/facebook-sdk.php
0 → 100644
| 1 | <?php | ||
| 2 | |||
| 3 | namespace FB; | ||
| 4 | use Exception; | ||
| 5 | |||
| 6 | if (!function_exists('curl_init')) { | ||
| 7 | throw new Exception('Facebook needs the CURL PHP extension.'); | ||
| 8 | } | ||
| 9 | if (!function_exists('json_decode')) { | ||
| 10 | throw new Exception('Facebook needs the JSON PHP extension.'); | ||
| 11 | } | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Thrown when an API call returns an exception. | ||
| 15 | * | ||
| 16 | * @author Naitik Shah <naitik@facebook.com> | ||
| 17 | */ | ||
| 18 | class FacebookApiException extends Exception | ||
| 19 | { | ||
| 20 | /** | ||
| 21 | * The result from the API server that represents the exception information. | ||
| 22 | */ | ||
| 23 | protected $result; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * Make a new API Exception with the given result. | ||
| 27 | * | ||
| 28 | * @param Array $result the result from the API server | ||
| 29 | */ | ||
| 30 | public function __construct($result) { | ||
| 31 | $this->result = $result; | ||
| 32 | |||
| 33 | $code = isset($result['error_code']) ? $result['error_code'] : 0; | ||
| 34 | $msg = isset($result['error']) | ||
| 35 | ? $result['error']['message'] : $result['error_msg']; | ||
| 36 | parent::__construct($msg, $code); | ||
| 37 | } | ||
| 38 | |||
| 39 | /** | ||
| 40 | * Return the associated result object returned by the API server. | ||
| 41 | * | ||
| 42 | * @returns Array the result from the API server | ||
| 43 | */ | ||
| 44 | public function getResult() { | ||
| 45 | return $this->result; | ||
| 46 | } | ||
| 47 | |||
| 48 | /** | ||
| 49 | * Returns the associated type for the error. This will default to | ||
| 50 | * 'Exception' when a type is not available. | ||
| 51 | * | ||
| 52 | * @return String | ||
| 53 | */ | ||
| 54 | public function getType() { | ||
| 55 | return | ||
| 56 | isset($this->result['error']) && isset($this->result['error']['type']) | ||
| 57 | ? $this->result['error']['type'] | ||
| 58 | : 'Exception'; | ||
| 59 | } | ||
| 60 | |||
| 61 | /** | ||
| 62 | * To make debugging easier. | ||
| 63 | * | ||
| 64 | * @returns String the string representation of the error | ||
| 65 | */ | ||
| 66 | public function __toString() { | ||
| 67 | $str = $this->getType() . ': '; | ||
| 68 | if ($this->code != 0) { | ||
| 69 | $str .= $this->code . ': '; | ||
| 70 | } | ||
| 71 | return $str . $this->message; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | /** | ||
| 76 | * Provides access to the Facebook Platform. | ||
| 77 | * | ||
| 78 | * @author Naitik Shah <naitik@facebook.com> | ||
| 79 | */ | ||
| 80 | class Facebook | ||
| 81 | { | ||
| 82 | /** | ||
| 83 | * Version. | ||
| 84 | */ | ||
| 85 | const VERSION = '2.0.5'; | ||
| 86 | |||
| 87 | /** | ||
| 88 | * Default options for curl. | ||
| 89 | */ | ||
| 90 | public static $CURL_OPTS = array( | ||
| 91 | CURLOPT_CONNECTTIMEOUT => 10, | ||
| 92 | CURLOPT_RETURNTRANSFER => true, | ||
| 93 | CURLOPT_TIMEOUT => 60, | ||
| 94 | CURLOPT_USERAGENT => 'facebook-php-2.0', | ||
| 95 | ); | ||
| 96 | |||
| 97 | /** | ||
| 98 | * List of query parameters that get automatically dropped when rebuilding | ||
| 99 | * the current URL. | ||
| 100 | */ | ||
| 101 | protected static $DROP_QUERY_PARAMS = array( | ||
| 102 | 'session', | ||
| 103 | ); | ||
| 104 | |||
| 105 | /** | ||
| 106 | * Maps aliases to Facebook domains. | ||
| 107 | */ | ||
| 108 | public static $DOMAIN_MAP = array( | ||
| 109 | 'api' => 'https://api.facebook.com/', | ||
| 110 | 'api_read' => 'https://api-read.facebook.com/', | ||
| 111 | 'graph' => 'https://graph.facebook.com/', | ||
| 112 | 'www' => 'https://www.facebook.com/', | ||
| 113 | ); | ||
| 114 | |||
| 115 | /** | ||
| 116 | * The Application ID. | ||
| 117 | */ | ||
| 118 | protected $appId; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * The Application API Secret. | ||
| 122 | */ | ||
| 123 | protected $apiSecret; | ||
| 124 | |||
| 125 | /** | ||
| 126 | * The active user session, if one is available. | ||
| 127 | */ | ||
| 128 | protected $session; | ||
| 129 | |||
| 130 | /** | ||
| 131 | * Indicates that we already loaded the session as best as we could. | ||
| 132 | */ | ||
| 133 | protected $sessionLoaded = false; | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Indicates if Cookie support should be enabled. | ||
| 137 | */ | ||
| 138 | protected $cookieSupport = false; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Base domain for the Cookie. | ||
| 142 | */ | ||
| 143 | protected $baseDomain = ''; | ||
| 144 | |||
| 145 | /** | ||
| 146 | * Initialize a Facebook Application. | ||
| 147 | * | ||
| 148 | * The configuration: | ||
| 149 | * - appId: the application ID | ||
| 150 | * - secret: the application secret | ||
| 151 | * - cookie: (optional) boolean true to enable cookie support | ||
| 152 | * - domain: (optional) domain for the cookie | ||
| 153 | * | ||
| 154 | * @param Array $config the application configuration | ||
| 155 | */ | ||
| 156 | public function __construct($config) { | ||
| 157 | $this->setAppId($config['appId']); | ||
| 158 | $this->setApiSecret($config['secret']); | ||
| 159 | if (isset($config['cookie'])) { | ||
| 160 | $this->setCookieSupport($config['cookie']); | ||
| 161 | } | ||
| 162 | if (isset($config['domain'])) { | ||
| 163 | $this->setBaseDomain($config['domain']); | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | /** | ||
| 168 | * Set the Application ID. | ||
| 169 | * | ||
| 170 | * @param String $appId the Application ID | ||
| 171 | */ | ||
| 172 | public function setAppId($appId) { | ||
| 173 | $this->appId = $appId; | ||
| 174 | return $this; | ||
| 175 | } | ||
| 176 | |||
| 177 | /** | ||
| 178 | * Get the Application ID. | ||
| 179 | * | ||
| 180 | * @return String the Application ID | ||
| 181 | */ | ||
| 182 | public function getAppId() { | ||
| 183 | return $this->appId; | ||
| 184 | } | ||
| 185 | |||
| 186 | /** | ||
| 187 | * Set the API Secret. | ||
| 188 | * | ||
| 189 | * @param String $appId the API Secret | ||
| 190 | */ | ||
| 191 | public function setApiSecret($apiSecret) { | ||
| 192 | $this->apiSecret = $apiSecret; | ||
| 193 | return $this; | ||
| 194 | } | ||
| 195 | |||
| 196 | /** | ||
| 197 | * Get the API Secret. | ||
| 198 | * | ||
| 199 | * @return String the API Secret | ||
| 200 | */ | ||
| 201 | public function getApiSecret() { | ||
| 202 | return $this->apiSecret; | ||
| 203 | } | ||
| 204 | |||
| 205 | /** | ||
| 206 | * Set the Cookie Support status. | ||
| 207 | * | ||
| 208 | * @param Boolean $cookieSupport the Cookie Support status | ||
| 209 | */ | ||
| 210 | public function setCookieSupport($cookieSupport) { | ||
| 211 | $this->cookieSupport = $cookieSupport; | ||
| 212 | return $this; | ||
| 213 | } | ||
| 214 | |||
| 215 | /** | ||
| 216 | * Get the Cookie Support status. | ||
| 217 | * | ||
| 218 | * @return Boolean the Cookie Support status | ||
| 219 | */ | ||
| 220 | public function useCookieSupport() { | ||
| 221 | return $this->cookieSupport; | ||
| 222 | } | ||
| 223 | |||
| 224 | /** | ||
| 225 | * Set the base domain for the Cookie. | ||
| 226 | * | ||
| 227 | * @param String $domain the base domain | ||
| 228 | */ | ||
| 229 | public function setBaseDomain($domain) { | ||
| 230 | $this->baseDomain = $domain; | ||
| 231 | return $this; | ||
| 232 | } | ||
| 233 | |||
| 234 | /** | ||
| 235 | * Get the base domain for the Cookie. | ||
| 236 | * | ||
| 237 | * @return String the base domain | ||
| 238 | */ | ||
| 239 | public function getBaseDomain() { | ||
| 240 | return $this->baseDomain; | ||
| 241 | } | ||
| 242 | |||
| 243 | /** | ||
| 244 | * Set the Session. | ||
| 245 | * | ||
| 246 | * @param Array $session the session | ||
| 247 | * @param Boolean $write_cookie indicate if a cookie should be written. this | ||
| 248 | * value is ignored if cookie support has been disabled. | ||
| 249 | */ | ||
| 250 | public function setSession($session=null, $write_cookie=true) { | ||
| 251 | $session = $this->validateSessionObject($session); | ||
| 252 | $this->sessionLoaded = true; | ||
| 253 | $this->session = $session; | ||
| 254 | if ($write_cookie) { | ||
| 255 | $this->setCookieFromSession($session); | ||
| 256 | } | ||
| 257 | return $this; | ||
| 258 | } | ||
| 259 | |||
| 260 | /** | ||
| 261 | * Get the session object. This will automatically look for a signed session | ||
| 262 | * sent via the Cookie or Query Parameters if needed. | ||
| 263 | * | ||
| 264 | * @return Array the session | ||
| 265 | */ | ||
| 266 | public function getSession() { | ||
| 267 | if (!$this->sessionLoaded) { | ||
| 268 | $session = null; | ||
| 269 | $write_cookie = true; | ||
| 270 | |||
| 271 | // try loading session from $_REQUEST | ||
| 272 | if (isset($_REQUEST['session'])) { | ||
| 273 | $session = json_decode( | ||
| 274 | get_magic_quotes_gpc() | ||
| 275 | ? stripslashes($_REQUEST['session']) | ||
| 276 | : $_REQUEST['session'], | ||
| 277 | true | ||
| 278 | ); | ||
| 279 | $session = $this->validateSessionObject($session); | ||
| 280 | } | ||
| 281 | |||
| 282 | // try loading session from cookie if necessary | ||
| 283 | if (!$session && $this->useCookieSupport()) { | ||
| 284 | $cookieName = $this->getSessionCookieName(); | ||
| 285 | if (isset($_COOKIE[$cookieName])) { | ||
| 286 | $session = array(); | ||
| 287 | parse_str(trim( | ||
| 288 | get_magic_quotes_gpc() | ||
| 289 | ? stripslashes($_COOKIE[$cookieName]) | ||
| 290 | : $_COOKIE[$cookieName], | ||
| 291 | '"' | ||
| 292 | ), $session); | ||
| 293 | $session = $this->validateSessionObject($session); | ||
| 294 | // write only if we need to delete a invalid session cookie | ||
| 295 | $write_cookie = empty($session); | ||
| 296 | } | ||
| 297 | } | ||
| 298 | |||
| 299 | $this->setSession($session, $write_cookie); | ||
| 300 | } | ||
| 301 | |||
| 302 | return $this->session; | ||
| 303 | } | ||
| 304 | |||
| 305 | /** | ||
| 306 | * Get the UID from the session. | ||
| 307 | * | ||
| 308 | * @return String the UID if available | ||
| 309 | */ | ||
| 310 | public function getUser() { | ||
| 311 | $session = $this->getSession(); | ||
| 312 | return $session ? $session['uid'] : null; | ||
| 313 | } | ||
| 314 | |||
| 315 | /** | ||
| 316 | * Get a Login URL for use with redirects. By default, full page redirect is | ||
| 317 | * assumed. If you are using the generated URL with a window.open() call in | ||
| 318 | * JavaScript, you can pass in display=popup as part of the $params. | ||
| 319 | * | ||
| 320 | * The parameters: | ||
| 321 | * - next: the url to go to after a successful login | ||
| 322 | * - cancel_url: the url to go to after the user cancels | ||
| 323 | * - req_perms: comma separated list of requested extended perms | ||
| 324 | * - display: can be "page" (default, full page) or "popup" | ||
| 325 | * | ||
| 326 | * @param Array $params provide custom parameters | ||
| 327 | * @return String the URL for the login flow | ||
| 328 | */ | ||
| 329 | public function getLoginUrl($params=array()) { | ||
| 330 | $currentUrl = $this->getCurrentUrl(); | ||
| 331 | return $this->getUrl( | ||
| 332 | 'www', | ||
| 333 | 'login.php', | ||
| 334 | array_merge(array( | ||
| 335 | 'api_key' => $this->getAppId(), | ||
| 336 | 'cancel_url' => $currentUrl, | ||
| 337 | 'display' => 'page', | ||
| 338 | 'fbconnect' => 1, | ||
| 339 | 'next' => $currentUrl, | ||
| 340 | 'return_session' => 1, | ||
| 341 | 'session_version' => 3, | ||
| 342 | 'v' => '1.0', | ||
| 343 | ), $params) | ||
| 344 | ); | ||
| 345 | } | ||
| 346 | |||
| 347 | /** | ||
| 348 | * Get a Logout URL suitable for use with redirects. | ||
| 349 | * | ||
| 350 | * The parameters: | ||
| 351 | * - next: the url to go to after a successful logout | ||
| 352 | * | ||
| 353 | * @param Array $params provide custom parameters | ||
| 354 | * @return String the URL for the logout flow | ||
| 355 | */ | ||
| 356 | public function getLogoutUrl($params=array()) { | ||
| 357 | $session = $this->getSession(); | ||
| 358 | return $this->getUrl( | ||
| 359 | 'www', | ||
| 360 | 'logout.php', | ||
| 361 | array_merge(array( | ||
| 362 | 'api_key' => $this->getAppId(), | ||
| 363 | 'next' => $this->getCurrentUrl(), | ||
| 364 | 'session_key' => $session['session_key'], | ||
| 365 | ), $params) | ||
| 366 | ); | ||
| 367 | } | ||
| 368 | |||
| 369 | /** | ||
| 370 | * Get a login status URL to fetch the status from facebook. | ||
| 371 | * | ||
| 372 | * The parameters: | ||
| 373 | * - ok_session: the URL to go to if a session is found | ||
| 374 | * - no_session: the URL to go to if the user is not connected | ||
| 375 | * - no_user: the URL to go to if the user is not signed into facebook | ||
| 376 | * | ||
| 377 | * @param Array $params provide custom parameters | ||
| 378 | * @return String the URL for the logout flow | ||
| 379 | */ | ||
| 380 | public function getLoginStatusUrl($params=array()) { | ||
| 381 | return $this->getUrl( | ||
| 382 | 'www', | ||
| 383 | 'extern/login_status.php', | ||
| 384 | array_merge(array( | ||
| 385 | 'api_key' => $this->getAppId(), | ||
| 386 | 'no_session' => $this->getCurrentUrl(), | ||
| 387 | 'no_user' => $this->getCurrentUrl(), | ||
| 388 | 'ok_session' => $this->getCurrentUrl(), | ||
| 389 | 'session_version' => 3, | ||
| 390 | ), $params) | ||
| 391 | ); | ||
| 392 | } | ||
| 393 | |||
| 394 | /** | ||
| 395 | * Make an API call. | ||
| 396 | * | ||
| 397 | * @param Array $params the API call parameters | ||
| 398 | * @return the decoded response | ||
| 399 | */ | ||
| 400 | public function api(/* polymorphic */) { | ||
| 401 | $args = func_get_args(); | ||
| 402 | if (is_array($args[0])) { | ||
| 403 | return $this->_restserver($args[0]); | ||
| 404 | } else { | ||
| 405 | return call_user_func_array(array($this, '_graph'), $args); | ||
| 406 | } | ||
| 407 | } | ||
| 408 | |||
| 409 | /** | ||
| 410 | * Invoke the old restserver.php endpoint. | ||
| 411 | * | ||
| 412 | * @param Array $params method call object | ||
| 413 | * @return the decoded response object | ||
| 414 | * @throws FacebookApiException | ||
| 415 | */ | ||
| 416 | protected function _restserver($params) { | ||
| 417 | // generic application level parameters | ||
| 418 | $params['api_key'] = $this->getAppId(); | ||
| 419 | $params['format'] = 'json-strings'; | ||
| 420 | |||
| 421 | $result = json_decode($this->_oauthRequest( | ||
| 422 | $this->getApiUrl($params['method']), | ||
| 423 | $params | ||
| 424 | ), true); | ||
| 425 | |||
| 426 | // results are returned, errors are thrown | ||
| 427 | if (is_array($result) && isset($result['error_code'])) { | ||
| 428 | throw new FacebookApiException($result); | ||
| 429 | } | ||
| 430 | return $result; | ||
| 431 | } | ||
| 432 | |||
| 433 | /** | ||
| 434 | * Invoke the Graph API. | ||
| 435 | * | ||
| 436 | * @param String $path the path (required) | ||
| 437 | * @param String $method the http method (default 'GET') | ||
| 438 | * @param Array $params the query/post data | ||
| 439 | * @return the decoded response object | ||
| 440 | * @throws FacebookApiException | ||
| 441 | */ | ||
| 442 | protected function _graph($path, $method='GET', $params=array()) { | ||
| 443 | if (is_array($method) && empty($params)) { | ||
| 444 | $params = $method; | ||
| 445 | $method = 'GET'; | ||
| 446 | } | ||
| 447 | $params['method'] = $method; // method override as we always do a POST | ||
| 448 | |||
| 449 | $result = json_decode($this->_oauthRequest( | ||
| 450 | $this->getUrl('graph', $path), | ||
| 451 | $params | ||
| 452 | ), true); | ||
| 453 | |||
| 454 | // results are returned, errors are thrown | ||
| 455 | if (is_array($result) && isset($result['error'])) { | ||
| 456 | $e = new FacebookApiException($result); | ||
| 457 | if ($e->getType() === 'OAuthException') { | ||
| 458 | $this->setSession(null); | ||
| 459 | } | ||
| 460 | throw $e; | ||
| 461 | } | ||
| 462 | return $result; | ||
| 463 | } | ||
| 464 | |||
| 465 | /** | ||
| 466 | * Make a OAuth Request | ||
| 467 | * | ||
| 468 | * @param String $path the path (required) | ||
| 469 | * @param Array $params the query/post data | ||
| 470 | * @return the decoded response object | ||
| 471 | * @throws FacebookApiException | ||
| 472 | */ | ||
| 473 | protected function _oauthRequest($url, $params) { | ||
| 474 | if (!isset($params['access_token'])) { | ||
| 475 | $session = $this->getSession(); | ||
| 476 | // either user session signed, or app signed | ||
| 477 | if ($session) { | ||
| 478 | $params['access_token'] = $session['access_token']; | ||
| 479 | } else { | ||
| 480 | $params['access_token'] = $this->getAppId() .'|'. $this->getApiSecret(); | ||
| 481 | } | ||
| 482 | } | ||
| 483 | |||
| 484 | // json_encode all params values that are not strings | ||
| 485 | foreach ($params as $key => $value) { | ||
| 486 | if (!is_string($value)) { | ||
| 487 | $params[$key] = json_encode($value); | ||
| 488 | } | ||
| 489 | } | ||
| 490 | return $this->makeRequest($url, $params); | ||
| 491 | } | ||
| 492 | |||
| 493 | /** | ||
| 494 | * Makes an HTTP request. This method can be overriden by subclasses if | ||
| 495 | * developers want to do fancier things or use something other than curl to | ||
| 496 | * make the request. | ||
| 497 | * | ||
| 498 | * @param String $url the URL to make the request to | ||
| 499 | * @param Array $params the parameters to use for the POST body | ||
| 500 | * @param CurlHandler $ch optional initialized curl handle | ||
| 501 | * @return String the response text | ||
| 502 | */ | ||
| 503 | protected function makeRequest($url, $params, $ch=null) { | ||
| 504 | if (!$ch) { | ||
| 505 | $ch = curl_init(); | ||
| 506 | } | ||
| 507 | |||
| 508 | $opts = self::$CURL_OPTS; | ||
| 509 | $opts[CURLOPT_POSTFIELDS] = http_build_query($params, null, '&'); | ||
| 510 | $opts[CURLOPT_URL] = $url; | ||
| 511 | curl_setopt_array($ch, $opts); | ||
| 512 | $result = curl_exec($ch); | ||
| 513 | if ($result === false) { | ||
| 514 | $e = new FacebookApiException(array( | ||
| 515 | 'error_code' => curl_errno($ch), | ||
| 516 | 'error' => array( | ||
| 517 | 'message' => curl_error($ch), | ||
| 518 | 'type' => 'CurlException', | ||
| 519 | ), | ||
| 520 | )); | ||
| 521 | curl_close($ch); | ||
| 522 | throw $e; | ||
| 523 | } | ||
| 524 | curl_close($ch); | ||
| 525 | return $result; | ||
| 526 | } | ||
| 527 | |||
| 528 | /** | ||
| 529 | * The name of the Cookie that contains the session. | ||
| 530 | * | ||
| 531 | * @return String the cookie name | ||
| 532 | */ | ||
| 533 | protected function getSessionCookieName() { | ||
| 534 | return 'fbs_' . $this->getAppId(); | ||
| 535 | } | ||
| 536 | |||
| 537 | /** | ||
| 538 | * Set a JS Cookie based on the _passed in_ session. It does not use the | ||
| 539 | * currently stored session -- you need to explicitly pass it in. | ||
| 540 | * | ||
| 541 | * @param Array $session the session to use for setting the cookie | ||
| 542 | */ | ||
| 543 | protected function setCookieFromSession($session=null) { | ||
| 544 | if (!$this->useCookieSupport()) { | ||
| 545 | return; | ||
| 546 | } | ||
| 547 | |||
| 548 | $cookieName = $this->getSessionCookieName(); | ||
| 549 | $value = 'deleted'; | ||
| 550 | $expires = time() - 3600; | ||
| 551 | $domain = $this->getBaseDomain(); | ||
| 552 | if ($session) { | ||
| 553 | $value = '"' . http_build_query($session, null, '&') . '"'; | ||
| 554 | if (isset($session['base_domain'])) { | ||
| 555 | $domain = $session['base_domain']; | ||
| 556 | } | ||
| 557 | $expires = $session['expires']; | ||
| 558 | } | ||
| 559 | |||
| 560 | // prepend dot if a domain is found | ||
| 561 | if ($domain) { | ||
| 562 | $domain = '.' . $domain; | ||
| 563 | } | ||
| 564 | |||
| 565 | // if an existing cookie is not set, we dont need to delete it | ||
| 566 | if ($value == 'deleted' && empty($_COOKIE[$cookieName])) { | ||
| 567 | return; | ||
| 568 | } | ||
| 569 | |||
| 570 | if (headers_sent()) { | ||
| 571 | self::error_log('Could not set cookie. Headers already sent.'); | ||
| 572 | |||
| 573 | // ignore for code coverage as we will never be able to setcookie in a CLI | ||
| 574 | // environment | ||
| 575 | // @codeCoverageIgnoreStart | ||
| 576 | } else { | ||
| 577 | setcookie($cookieName, $value, $expires, '/', $domain); | ||
| 578 | } | ||
| 579 | // @codeCoverageIgnoreEnd | ||
| 580 | } | ||
| 581 | |||
| 582 | /** | ||
| 583 | * Validates a session_version=3 style session object. | ||
| 584 | * | ||
| 585 | * @param Array $session the session object | ||
| 586 | * @return Array the session object if it validates, null otherwise | ||
| 587 | */ | ||
| 588 | protected function validateSessionObject($session) { | ||
| 589 | // make sure some essential fields exist | ||
| 590 | if (is_array($session) && | ||
| 591 | isset($session['uid']) && | ||
| 592 | isset($session['session_key']) && | ||
| 593 | isset($session['secret']) && | ||
| 594 | isset($session['access_token']) && | ||
| 595 | isset($session['sig'])) { | ||
| 596 | // validate the signature | ||
| 597 | $session_without_sig = $session; | ||
| 598 | unset($session_without_sig['sig']); | ||
| 599 | $expected_sig = self::generateSignature( | ||
| 600 | $session_without_sig, | ||
| 601 | $this->getApiSecret() | ||
| 602 | ); | ||
| 603 | if ($session['sig'] != $expected_sig) { | ||
| 604 | self::error_log('Got invalid session signature in cookie.'); | ||
| 605 | $session = null; | ||
| 606 | } | ||
| 607 | // check expiry time | ||
| 608 | } else { | ||
| 609 | $session = null; | ||
| 610 | } | ||
| 611 | return $session; | ||
| 612 | } | ||
| 613 | |||
| 614 | /** | ||
| 615 | * Build the URL for api given parameters. | ||
| 616 | * | ||
| 617 | * @param $method String the method name. | ||
| 618 | * @return String the URL for the given parameters | ||
| 619 | */ | ||
| 620 | protected function getApiUrl($method) { | ||
| 621 | static $READ_ONLY_CALLS = | ||
| 622 | array('admin.getallocation' => 1, | ||
| 623 | 'admin.getappproperties' => 1, | ||
| 624 | 'admin.getbannedusers' => 1, | ||
| 625 | 'admin.getlivestreamvialink' => 1, | ||
| 626 | 'admin.getmetrics' => 1, | ||
| 627 | 'admin.getrestrictioninfo' => 1, | ||
| 628 | 'application.getpublicinfo' => 1, | ||
| 629 | 'auth.getapppublickey' => 1, | ||
| 630 | 'auth.getsession' => 1, | ||
| 631 | 'auth.getsignedpublicsessiondata' => 1, | ||
| 632 | 'comments.get' => 1, | ||
| 633 | 'connect.getunconnectedfriendscount' => 1, | ||
| 634 | 'dashboard.getactivity' => 1, | ||
| 635 | 'dashboard.getcount' => 1, | ||
| 636 | 'dashboard.getglobalnews' => 1, | ||
| 637 | 'dashboard.getnews' => 1, | ||
| 638 | 'dashboard.multigetcount' => 1, | ||
| 639 | 'dashboard.multigetnews' => 1, | ||
| 640 | 'data.getcookies' => 1, | ||
| 641 | 'events.get' => 1, | ||
| 642 | 'events.getmembers' => 1, | ||
| 643 | 'fbml.getcustomtags' => 1, | ||
| 644 | 'feed.getappfriendstories' => 1, | ||
| 645 | 'feed.getregisteredtemplatebundlebyid' => 1, | ||
| 646 | 'feed.getregisteredtemplatebundles' => 1, | ||
| 647 | 'fql.multiquery' => 1, | ||
| 648 | 'fql.query' => 1, | ||
| 649 | 'friends.arefriends' => 1, | ||
| 650 | 'friends.get' => 1, | ||
| 651 | 'friends.getappusers' => 1, | ||
| 652 | 'friends.getlists' => 1, | ||
| 653 | 'friends.getmutualfriends' => 1, | ||
| 654 | 'gifts.get' => 1, | ||
| 655 | 'groups.get' => 1, | ||
| 656 | 'groups.getmembers' => 1, | ||
| 657 | 'intl.gettranslations' => 1, | ||
| 658 | 'links.get' => 1, | ||
| 659 | 'notes.get' => 1, | ||
| 660 | 'notifications.get' => 1, | ||
| 661 | 'pages.getinfo' => 1, | ||
| 662 | 'pages.isadmin' => 1, | ||
| 663 | 'pages.isappadded' => 1, | ||
| 664 | 'pages.isfan' => 1, | ||
| 665 | 'permissions.checkavailableapiaccess' => 1, | ||
| 666 | 'permissions.checkgrantedapiaccess' => 1, | ||
| 667 | 'photos.get' => 1, | ||
| 668 | 'photos.getalbums' => 1, | ||
| 669 | 'photos.gettags' => 1, | ||
| 670 | 'profile.getinfo' => 1, | ||
| 671 | 'profile.getinfooptions' => 1, | ||
| 672 | 'stream.get' => 1, | ||
| 673 | 'stream.getcomments' => 1, | ||
| 674 | 'stream.getfilters' => 1, | ||
| 675 | 'users.getinfo' => 1, | ||
| 676 | 'users.getloggedinuser' => 1, | ||
| 677 | 'users.getstandardinfo' => 1, | ||
| 678 | 'users.hasapppermission' => 1, | ||
| 679 | 'users.isappuser' => 1, | ||
| 680 | 'users.isverified' => 1, | ||
| 681 | 'video.getuploadlimits' => 1); | ||
| 682 | $name = 'api'; | ||
| 683 | if (isset($READ_ONLY_CALLS[strtolower($method)])) { | ||
| 684 | $name = 'api_read'; | ||
| 685 | } | ||
| 686 | return self::getUrl($name, 'restserver.php'); | ||
| 687 | } | ||
| 688 | |||
| 689 | /** | ||
| 690 | * Build the URL for given domain alias, path and parameters. | ||
| 691 | * | ||
| 692 | * @param $name String the name of the domain | ||
| 693 | * @param $path String optional path (without a leading slash) | ||
| 694 | * @param $params Array optional query parameters | ||
| 695 | * @return String the URL for the given parameters | ||
| 696 | */ | ||
| 697 | protected function getUrl($name, $path='', $params=array()) { | ||
| 698 | $url = self::$DOMAIN_MAP[$name]; | ||
| 699 | if ($path) { | ||
| 700 | if ($path[0] === '/') { | ||
| 701 | $path = substr($path, 1); | ||
| 702 | } | ||
| 703 | $url .= $path; | ||
| 704 | } | ||
| 705 | if ($params) { | ||
| 706 | $url .= '?' . http_build_query($params); | ||
| 707 | } | ||
| 708 | return $url; | ||
| 709 | } | ||
| 710 | |||
| 711 | /** | ||
| 712 | * Returns the Current URL, stripping it of known FB parameters that should | ||
| 713 | * not persist. | ||
| 714 | * | ||
| 715 | * @return String the current URL | ||
| 716 | */ | ||
| 717 | protected function getCurrentUrl() { | ||
| 718 | $protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' | ||
| 719 | ? 'https://' | ||
| 720 | : 'http://'; | ||
| 721 | $currentUrl = $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; | ||
| 722 | $parts = parse_url($currentUrl); | ||
| 723 | |||
| 724 | // drop known fb params | ||
| 725 | $query = ''; | ||
| 726 | if (!empty($parts['query'])) { | ||
| 727 | $params = array(); | ||
| 728 | parse_str($parts['query'], $params); | ||
| 729 | foreach(self::$DROP_QUERY_PARAMS as $key) { | ||
| 730 | unset($params[$key]); | ||
| 731 | } | ||
| 732 | if (!empty($params)) { | ||
| 733 | $query = '?' . http_build_query($params); | ||
| 734 | } | ||
| 735 | } | ||
| 736 | |||
| 737 | // use port if non default | ||
| 738 | $port = | ||
| 739 | isset($parts['port']) && | ||
| 740 | (($protocol === 'http://' && $parts['port'] !== 80) || | ||
| 741 | ($protocol === 'https://' && $parts['port'] !== 443)) | ||
| 742 | ? ':' . $parts['port'] : ''; | ||
| 743 | |||
| 744 | // rebuild | ||
| 745 | return $protocol . $parts['host'] . $port . $parts['path'] . $query; | ||
| 746 | } | ||
| 747 | |||
| 748 | /** | ||
| 749 | * Generate a signature for the given params and secret. | ||
| 750 | * | ||
| 751 | * @param Array $params the parameters to sign | ||
| 752 | * @param String $secret the secret to sign with | ||
| 753 | * @return String the generated signature | ||
| 754 | */ | ||
| 755 | protected static function generateSignature($params, $secret) { | ||
| 756 | // work with sorted data | ||
| 757 | ksort($params); | ||
| 758 | |||
| 759 | // generate the base string | ||
| 760 | $base_string = ''; | ||
| 761 | foreach($params as $key => $value) { | ||
| 762 | $base_string .= $key . '=' . $value; | ||
| 763 | } | ||
| 764 | $base_string .= $secret; | ||
| 765 | |||
| 766 | return md5($base_string); | ||
| 767 | } | ||
| 768 | |||
| 769 | /** | ||
| 770 | * Prints to the error log if you aren't in command line mode. | ||
| 771 | * | ||
| 772 | * @param String log message | ||
| 773 | */ | ||
| 774 | protected static function error_log($msg) { | ||
| 775 | // disable error log if we are running in a CLI environment | ||
| 776 | // @codeCoverageIgnoreStart | ||
| 777 | if (php_sapi_name() != 'cli') { | ||
| 778 | error_log($msg); | ||
| 779 | } | ||
| 780 | // uncomment this if you want to see the errors on the page | ||
| 781 | // print 'error_log: '.$msg."\n"; | ||
| 782 | // @codeCoverageIgnoreEnd | ||
| 783 | } | ||
| 784 | } | ||
| 785 | ?> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
com/Auth/Settings.php
0 → 100644
| 1 | <?php | ||
| 2 | namespace Tz\WordPress\Tools\Auth\Settings; | ||
| 3 | |||
| 4 | use Tz\Common; | ||
| 5 | use Tz\WordPress\Tools; | ||
| 6 | use Tz\WordPress\Tools\Auth; | ||
| 7 | |||
| 8 | const OPTION_GROUP = 'auth_group'; // Grouping of options used for setting output by WP | ||
| 9 | const OPTION_SECTION = 'tz_auth_main'; | ||
| 10 | const ADMIN_PAGE = 'tz-tools-auth'; // URI of options page | ||
| 11 | const CAPABILITY = 'manage_auth'; // User & Roles capability name | ||
| 12 | |||
| 13 | call_user_func(function() { | ||
| 14 | $role = get_role('administrator'); | ||
| 15 | $role->add_cap(CAPABILITY); | ||
| 16 | |||
| 17 | Tools\add_actions(__NAMESPACE__ . '\Actions'); | ||
| 18 | }); | ||
| 19 | |||
| 20 | function displayPage() { | ||
| 21 | require_once(__DIR__ . DIRECTORY_SEPARATOR . 'settings_view.php'); | ||
| 22 | } | ||
| 23 | |||
| 24 | function validate($data) { | ||
| 25 | return $data; | ||
| 26 | |||
| 27 | // Validation library causing some issues with Array | ||
| 28 | $valid = new Validation((array)$data); | ||
| 29 | return $valid->valid; | ||
| 30 | } | ||
| 31 | |||
| 32 | class Actions { | ||
| 33 | public static function admin_menu() { | ||
| 34 | add_options_page('Authentication', 'Authentication', CAPABILITY, ADMIN_PAGE, __NAMESPACE__ . '\displayPage'); | ||
| 35 | } | ||
| 36 | |||
| 37 | public static function admin_init() { | ||
| 38 | register_setting(OPTION_GROUP, Auth\OPTION_NAME, __NAMESPACE__ . '\validate'); | ||
| 39 | add_settings_section(OPTION_SECTION, 'Authentication Pages', function(){}, ADMIN_PAGE); | ||
| 40 | |||
| 41 | Tools\add_settings_fields(__NAMESPACE__ . '\Fields', ADMIN_PAGE, OPTION_SECTION); | ||
| 42 | } | ||
| 43 | } | ||
| 44 | |||
| 45 | class Fields { | ||
| 46 | public static function login_page() { | ||
| 47 | _dropdown_pages(Array('name' => Auth\OPTION_NAME . '[' . __FUNCTION__ . ']', 'sort_column' => 'menu_order', 'echo' => 1, 'selected' => Auth\Vars::$options[__FUNCTION__])); | ||
| 48 | } | ||
| 49 | |||
| 50 | public static function account_page() { | ||
| 51 | _dropdown_pages(Array('name' => Auth\OPTION_NAME . '[' . __FUNCTION__ . ']', 'sort_column' => 'menu_order', 'echo' => 1, 'selected' => Auth\Vars::$options[__FUNCTION__])); | ||
| 52 | } | ||
| 53 | |||
| 54 | public static function Facebook() { | ||
| 55 | echo '<input type="checkbox" id="' . __FUNCTION__ . '" name="' . Auth\OPTION_NAME . '[third_party][' . __FUNCTION__ . ']" value="1" ' . checked('1', Auth\Vars::$options['third_party'][__FUNCTION__], false) . ' />'; | ||
| 56 | } | ||
| 57 | } | ||
| 58 | |||
| 59 | class Validation extends Common\Validation { | ||
| 60 | public static function login_page($val) { | ||
| 61 | // if !is page throw exception | ||
| 62 | } | ||
| 63 | |||
| 64 | public static function account_page($val) { | ||
| 65 | // if !is page throw exception | ||
| 66 | // if is same as login_page throw exception | ||
| 67 | } | ||
| 68 | } | ||
| 69 | ?> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
com/Auth/settings_view.php
0 → 100644
| 1 | <?php | ||
| 2 | namespace Tz\WordPress\Tools\Auth\Settings; | ||
| 3 | ?> | ||
| 4 | <div class="wrap"> | ||
| 5 | <?php screen_icon(); ?> | ||
| 6 | <h2>Authentication Settings</h2> | ||
| 7 | |||
| 8 | <form method="post" action="options.php"> | ||
| 9 | <?php | ||
| 10 | settings_fields(OPTION_GROUP); | ||
| 11 | do_settings_sections(ADMIN_PAGE); | ||
| 12 | ?> | ||
| 13 | <p class="submit"><input type="submit" class="button-primary" value="Save Changes" /></p> | ||
| 14 | </form> | ||
| 15 | </div> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment