diff --git a/include/class-CronosHomepage.php b/include/class-CronosHomepage.php new file mode 100644 index 0000000..ca98288 --- /dev/null +++ b/include/class-CronosHomepage.php @@ -0,0 +1,424 @@ +. + +// OAuth PHP library stuff +use MediaWiki\OAuthClient\ClientConfig; +use MediaWiki\OAuthClient\Consumer; +use MediaWiki\OAuthClient\Client; +use MediaWiki\OAuthClient\Token; + +// boz-mw stuff +use web\MediaWikis; + +// various cookies +// we use cookies instead of PHP sessions because in this way the application is stateless +// moreover, Wikimedia Toolforge's NFS storage is not that quick :^) +define( 'COOKIE_OAUTH_REQUEST_TOKEN_KEY', 'oa_rqtk_key' ); +define( 'COOKIE_OAUTH_REQUEST_TOKEN_SECRET', 'oa_rqtk_sec' ); +define( 'COOKIE_OAUTH_ACCESS_TOKEN_KEY', 'oa_accsstk_key' ); +define( 'COOKIE_OAUTH_ACCESS_TOKEN_SECRET', 'oa_accsstk_secret' ); +define( 'COOKIE_OAUTH_ACCESS_NONCE', 'oa_accss_nnc' ); +define( 'COOKIE_WIKI_USERNAME', 'wiki_user' ); +define( 'COOKIE_WIKI_CSRF', 'wiki_csrf' ); + +class CronosHomepage { + + /** + * Check if the user has submitted the form + */ + private $saved = false; + + /** + * OAuth client + */ + private $client; + + /** + * Access token + */ + private $accessToken; + + /** + * The wiki API URL + * + * @var string + */ + private $wikiApi = 'https://meta.wikimedia.org/w/api.php'; + + /** + * Submitted form data + */ + private $post = []; + + /** + * Title of the events page of the current date + */ + private $eventsPageTitle; + + /** + * Constructor + */ + public function __construct() { + + // OAuth configuration + // see oauthclient-php + $conf = new ClientConfig( 'https://meta.wikimedia.org/w/index.php?title=Special:OAuth' ); + $conf->setConsumer( new Consumer( OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET ) ); + $this->client = new Client( $conf ); + + // Wikimedia Commons API + // see boz-mw + $commons = MediaWikis::findFromUID( 'commonswiki' ); + + $events_page_title = null; + + // Phase 1 + // check if the user submitted the login form and have to be redirected to the remote OAuth login form + if( is_action( 'login' ) ) { + + $this->tryOAuthLogin(); + } + + // check if the user wants to logout + if( is_action( 'logout' ) ) { + + $this->logout(); + + } + + // Phase 2 + // check if the user is coming back from the remote OAuth login form + if( isset( $_GET['oauth_verifier'] ) ) { + + $this->receiveOAuthResponse(); + + } + + // prepare the OAuth access token + $this->prepareOAuthAccessToken(); + + // Phase 3: save + if( $this->accessToken && is_action( 'create-event' ) ) { + + $this->createEvent(); + } + } + + public function isEventsPageTitleKnown() { + return isset( $this->eventsPageTitle ); + } + + public function getEventsPageTitle() { + return $this->eventsPageTitle; + } + + public function getEventsPageURL() { + return sprintf( + 'https://meta.wikimedia.org/wiki/%s', + urlencode( $this->getEventsPageTitle() ) + ); + } + + public function hasSaved() { + return $this->saved; + } + + public function isUserUnknown() { + return empty( $_COOKIE[ COOKIE_WIKI_USERNAME ] ); + } + + public function getAnnouncedUsername() { + return $_COOKIE[ COOKIE_WIKI_USERNAME ] ?? null; + } + + /** + * Print the website header + */ + public function printHeader() { + + enqueue_js( 'cronos' ); + + template( 'header' ); + + } + + /** + * Print the website footer + */ + public function printFooter() { + + template( 'footer' ); + + } + + /** + * Get a submitted information + * + * @param string $key + * @param string $default_value + */ + public function getPOST( $key, $default_value = null ) { + + // eventually receive submitted data + if( !array_key_exists( $key, $this->post ) ) { + + // technically it's possible to submit arrays (asd[]=1) so let's clean + if( is_string( $_POST[ $key ] ) ) { + $this->post[ $key ] = $_POST[ $key ]; + } + } + + return $this->post[ $key ] ?? $default_value; + } + + /** + * Forget this OAuth session + */ + private function logout() { + + // clear all cookies + my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_KEY ); + my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_SECRET ); + my_unset_cookie( COOKIE_OAUTH_ACCESS_TOKEN_KEY ); + my_unset_cookie( COOKIE_OAUTH_ACCESS_TOKEN_SECRET ); + my_unset_cookie( COOKIE_OAUTH_ACCESS_NONCE ); + my_unset_cookie( COOKIE_WIKI_USERNAME ); + my_unset_cookie( COOKIE_WIKI_CSRF ); + + // POST -> redirect -> GET + http_redirect( '' ); + } + + /** + * Redirect to the OAuth login page + */ + private function tryOAuthLogin() { + + // thanks for submitting the form! + list( $auth_url, $request_token ) = $this->client->initiate(); + + // remember the OAuth request token + my_set_cookie( COOKIE_OAUTH_REQUEST_TOKEN_KEY, $request_token->key ); + my_set_cookie( COOKIE_OAUTH_REQUEST_TOKEN_SECRET, $request_token->secret ); + + // here we go! + http_redirect( $auth_url ); + + } + + + /** + * Receive the OAuth response and redirect to the homepage again + */ + private function receiveOAuthResponse() { + + // welcome back, user, what was the original OAuth request token? + $request_token_key = $_COOKIE[ COOKIE_OAUTH_REQUEST_TOKEN_KEY ] ?? null; + $request_token_sec = $_COOKIE[ COOKIE_OAUTH_REQUEST_TOKEN_SECRET ] ?? null; + if( !$request_token_key || !$request_token_key ) { + throw new Exception( "missing request tokens" ); + } + + // rebuild the OAuth request token + $request_token = new Token( $request_token_key, $request_token_sec ); + + // check the OAuth access token + $access_token = $this->client->complete( $request_token, $_GET['oauth_verifier'] ); + $identity = $this->client->identify( $access_token ); + + // clear old cookies now unuseful + my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_KEY ); + my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_SECRET ); + + // save the access token to rebuild it later + my_set_cookie( COOKIE_OAUTH_ACCESS_TOKEN_KEY, $access_token->key ); + my_set_cookie( COOKIE_OAUTH_ACCESS_TOKEN_SECRET, $access_token->secret ); + + // save other information + my_set_cookie( COOKIE_OAUTH_ACCESS_NONCE, $identity->nonce ); + my_set_cookie( COOKIE_WIKI_USERNAME, $identity->username ); + + // clean the URL + http_redirect( '' ); + + } + + /** + * Prepare the OAuth access token from cookies + */ + private function prepareOAuthAccessToken() { + + // eventually build the OAuth access token + if( isset( + $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_KEY ], + $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_SECRET ] + ) ) { + $this->accessToken = new Token( + $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_KEY ], + $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_SECRET ] + ); + } + + } + + /** + * Request the wiki's CSRF + * + * @return string + */ + private function requestWikiCSRFToken() { + + // retrieve wiki CSRF token + $response_tokens = + $this->makeOAuthPOST( [ + 'action' => 'query', + 'format' => 'json', + 'meta' => 'tokens', + 'type' => 'csrf', + ] ); + + // no CSRF token no party + $csrf_token = $response_tokens->query->tokens->csrftoken ?? null; + if( !$csrf_token ) { + throw new Exception( "cannot retrieve CSRF token from wiki" ); + } + + return $csrf_token; + + } + + /** + * Try to create the Event from the POST-ed data + */ + private function createEvent() { + + // read POST-ed data + // assume that this data has sense (the user is logged-in and anyway he/she is just editing a page) + // in the worst of the cases, the event will be broken and a warning will be shown + $event_title = $this->getPOST( 'event_title' ); + $event_date_start = $this->getPOST( 'event_date_start' ); + $event_date_end = $this->getPOST( 'event_date_end' ); + $event_time_start = $this->getPOST( 'event_time_start' ); + $event_time_end = $this->getPOST( 'event_time_end' ); + $event_url = $this->getPOST( 'event_url' ); + + // no dates no party + if( empty( $event_date_start ) ) { + throw new Exception( "missing date start" ); + } + + // get the wiki CSRF token + $csrf_token = $this->requestWikiCSRFToken(); + if( !$csrf_token ) { + throw new Exception( "missing CSRF token from session" ); + } + + // this is the page that will host the event infobox + $this->eventsPageTitle = "Meta:Cronos/Events/$event_date_start"; + + // split date in parts + $event_date_start_parts = explode( '-', $event_date_start ); + if( count( $event_date_start_parts ) !== 3 ) { + throw new Exception( "bad start date" ); + } + list( $start_y, $start_m, $start_d ) = $event_date_start_parts; + + // text of the page + $text_create = sprintf( + '{{Cronos day|%d|%d|%d}}', + $start_y, + $start_m, + $start_d + ); + + $template_event = + "\n" . + "{{Cronos event\n" . + "| title = $event_title\n" . + "| when = $event_date_start $event_time_start\n" . + "| end = $event_date_end $event_time_end\n" . + "| url = $event_url\n" . + "}}"; + + $text_create .= $template_event; + + // try to create the page + $response = $this->makeOAuthPOST( [ + 'action' => 'edit', + 'createonly' => 1, + 'format' => 'json', + 'title' => $this->getEventsPageTitle(), + 'summary' => "New event: $event_title", + 'text' => $text_create, + 'token' => $csrf_token, + ] ); + + // eventually edit the page + $result = $response->edit->result ?? null; + + $this->saved = $result === 'Success'; + + // check if the page already exist and append text + if( !$this->saved ) { + + $error_code = $response->error->code ?? null; + if( $error_code === 'articleexists' ) { + + // create the page + $response = $this->makeOAuthPOST( [ + 'action' => 'edit', + 'nocreate' => 1, + 'format' => 'json', + 'title' => $this->getEventsPageTitle(), + 'summary' => "New event: $event_title", + 'appendtext' => $template_event, + 'token' => $csrf_token, + ] ); + + // assume success + $this->saved = true; + } else { + + throw new Exception( $error_code ); + + } + } + } + + /** + * Make an OAuth HTTP POST request + * + * @param array $data + * @return array + */ + private function makeOAuthPOST( $data ) { + + // try to make the HTTP request + $response_raw = $this->client->makeOAuthCall( $this->accessToken, $this->wikiApi, $isPost = true, $data ); + if( !$response_raw ) { + throw new Exception( "missing response" ); + } + + // try to parse JSON + $response = @json_decode( $response_raw ); + if( !$response ) { + throw new Exception( "response not a JSON" ); + } + + return $response; + + } +} diff --git a/load-post.php b/load-post.php index 5875e7e..48e5c84 100644 --- a/load-post.php +++ b/load-post.php @@ -1,30 +1,33 @@ . // this file is called after the suckless-php/load.php file // require some dummy functions require ABSPATH . '/include/functions.php'; +// require some dummy functions +require ABSPATH . '/include/class-CronosHomepage.php'; + // MaterializeCSS // https://materializecss.com/ register_js( 'materialize', 'static/materialize/js/materialize.min.js', 'footer' ); register_css( 'materialize', 'static/materialize/css/materialize.min.css' ); // register JavaScript files register_js( 'cronos', 'static/cronos.js', 'footer', [ 'materialize', ] ); diff --git a/www/index.php b/www/index.php index f87c650..e42c5be 100644 --- a/www/index.php +++ b/www/index.php @@ -1,406 +1,173 @@ . // load the configuration file and autoload classes require 'load.php'; -// OAuth PHP library stuff -use MediaWiki\OAuthClient\ClientConfig; -use MediaWiki\OAuthClient\Consumer; -use MediaWiki\OAuthClient\Client; -use MediaWiki\OAuthClient\Token; - -// boz-mw stuff -use web\MediaWikis; - -// various cookies -// we use cookies instead of PHP sessions because in this way the application is stateless -// moreover, Wikimedia Toolforge's NFS storage is not that quick :^) -define( 'COOKIE_OAUTH_REQUEST_TOKEN_KEY', 'oa_rqtk_key' ); -define( 'COOKIE_OAUTH_REQUEST_TOKEN_SECRET', 'oa_rqtk_sec' ); -define( 'COOKIE_OAUTH_ACCESS_TOKEN_KEY', 'oa_accsstk_key' ); -define( 'COOKIE_OAUTH_ACCESS_TOKEN_SECRET', 'oa_accsstk_secret' ); -define( 'COOKIE_OAUTH_ACCESS_NONCE', 'oa_accss_nnc' ); -define( 'COOKIE_WIKI_USERNAME', 'wiki_user' ); -define( 'COOKIE_WIKI_CSRF', 'wiki_csrf' ); - -// enqueue these JavaScript files -enqueue_js( 'cronos' ); - -// wiki API endpoint URL -$WIKI_API_URL = 'https://meta.wikimedia.org/w/api.php'; - -// Wikimedia Commons API -// see boz-mw -$commons = MediaWikis::findFromUID( 'commonswiki' ); - -// OAuth configuration -// see oauthclient-php -$conf = new ClientConfig( 'https://meta.wikimedia.org/w/index.php?title=Special:OAuth' ); -$conf->setConsumer( new Consumer( OAUTH_CONSUMER_KEY, OAUTH_CONSUMER_SECRET ) ); -$client = new Client( $conf ); - -// user identity -$identity = null; - -// read submitted parameters -$event_title = $_POST['event_title'] ?? null; -$event_date_start = $_POST['event_date_start'] ?? null; -$event_date_end = $_POST['event_date_end'] ?? null; -$event_time_start = $_POST['event_time_start'] ?? null; -$event_time_end = $_POST['event_time_end'] ?? null; -$event_url = $_POST['event_url'] ?? null; - -$saved = false; -$events_page_title = null; - -// Phase 1 -// check if the user submitted the login form and have to be redirected to the remote OAuth login form -if( is_action( 'login' ) ) { - - // thanks for submitting the form! - list( $auth_url, $request_token ) = $client->initiate(); - - // remember the OAuth request token - my_set_cookie( COOKIE_OAUTH_REQUEST_TOKEN_KEY, $request_token->key ); - my_set_cookie( COOKIE_OAUTH_REQUEST_TOKEN_SECRET, $request_token->secret ); - - // here we go! - http_redirect( $auth_url ); -} - -// Phase 2 -// check if the user is coming back from the remote OAuth login form -if( isset( $_GET['oauth_verifier'] ) ) { - - // welcome back, user, what was the original OAuth request token? - $request_token_key = $_COOKIE[ COOKIE_OAUTH_REQUEST_TOKEN_KEY ] ?? null; - $request_token_sec = $_COOKIE[ COOKIE_OAUTH_REQUEST_TOKEN_SECRET ] ?? null; - if( !$request_token_key || !$request_token_key ) { - throw new Exception( "missing request tokens" ); - } - - // rebuild the OAuth request token - $request_token = new Token( $request_token_key, $request_token_sec ); - - // check the OAuth access token - $access_token = $client->complete( $request_token, $_GET['oauth_verifier'] ); - $identity = $client->identify( $access_token ); - - // clear old cookies now unuseful - my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_KEY ); - my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_SECRET ); - - // save the access token to rebuild it later - my_set_cookie( COOKIE_OAUTH_ACCESS_TOKEN_KEY, $access_token->key ); - my_set_cookie( COOKIE_OAUTH_ACCESS_TOKEN_SECRET, $access_token->secret ); - - // save other information - my_set_cookie( COOKIE_OAUTH_ACCESS_NONCE, $identity->nonce ); - my_set_cookie( COOKIE_WIKI_USERNAME, $identity->username ); - - // POST -> redirect -> GET - http_redirect( '' ); -} - -// eventually build the OAuth access token -$access_token = null; -if( isset( - $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_KEY ], - $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_SECRET ] -) ) { - $access_token = new Token( - $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_KEY ], - $_COOKIE[ COOKIE_OAUTH_ACCESS_TOKEN_SECRET ] - ); -} - -// eventually retrieve the access token -$csrf_token = $_COOKIE[ COOKIE_WIKI_CSRF ] ?? null; -if( !$csrf_token && $access_token ) { - - // retrieve wiki CSRF token - $response_tokens = - $client->makeOAuthCall( - $access_token, - http_build_get_query( $WIKI_API_URL, [ - 'action' => 'query', - 'format' => 'json', - 'meta' => 'tokens', - 'type' => 'csrf', - ] ) - ); - - // no CSRF token no party - $csrf_token = @json_decode( $response_tokens )->query->tokens->csrftoken ?? null; - if( !$csrf_token ) { - throw new Exception( "cannot retrieve CSRF token from wiki" ); - } - - // remember the CSRF token for future requests - my_set_cookie( COOKIE_WIKI_CSRF, $csrf_token ); -} - -// Phase 3 -if( $access_token && is_action( 'create-event' ) ) { - - // no token no party - if( !$csrf_token ) { - throw new Exception( "missing CSRF token from session" ); - } - - // no dates no party - if( empty( $event_date_start ) ) { - throw new Exception( "missing date start" ); - } - - $events_page_title = "Meta:Cronos/Events/$event_date_start"; - - // split date in parts - $event_date_start_parts = explode( '-', $event_date_start ); - if( count( $event_date_start_parts ) !== 3 ) { - throw new Exception( "bad start date" ); - } - list( $start_y, $start_m, $start_d ) = $event_date_start_parts; - - // text of the page - $text_create = sprintf( - '{{Cronos day|%d|%d|%d}}', - $start_y, - $start_m, - $start_d - ); - - $template_event = - "\n" . - "{{Cronos event\n" . - "| title = $event_title\n" . - "| when = $event_date_start $event_time_start\n" . - "| end = $event_date_end $event_time_end\n" . - "| url = $event_url\n" . - "}}"; - - $text_create .= $template_event; - - // create the page - $response_raw = $client->makeOAuthCall( $access_token, $WIKI_API_URL, $isPost = true, [ - 'action' => 'edit', - 'createonly' => 1, - 'format' => 'json', - 'title' => $events_page_title, - 'summary' => "New event: $event_title", - 'text' => $text_create, - 'token' => $csrf_token, - ] ); - - // eventually edit the page - $response = @json_decode( $response_raw ); - $result = $response->edit->result ?? null; - $saved = $result === 'Success'; - - // check if the page already exist and append text - if( !$saved ) { - $error_code = $response->error->code ?? null; - if( $error_code === 'articleexists' ) { - - // create the page - $response_raw = $client->makeOAuthCall( $access_token, $WIKI_API_URL, $isPost = true, [ - 'action' => 'edit', - 'nocreate' => 1, - 'format' => 'json', - 'title' => $events_page_title, - 'summary' => "New event: $event_title", - 'appendtext' => $template_event, - 'token' => $csrf_token, - ] ); - - // assume success - $saved = true; - } - } -} - -// check if the user wants to logout -if( is_action( 'logout' ) ) { - - // clear all cookies - my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_KEY ); - my_unset_cookie( COOKIE_OAUTH_REQUEST_TOKEN_SECRET ); - my_unset_cookie( COOKIE_OAUTH_ACCESS_TOKEN_KEY ); - my_unset_cookie( COOKIE_OAUTH_ACCESS_TOKEN_SECRET ); - my_unset_cookie( COOKIE_OAUTH_ACCESS_NONCE ); - my_unset_cookie( COOKIE_WIKI_USERNAME ); - my_unset_cookie( COOKIE_WIKI_CSRF ); - - // POST -> redirect -> GET - http_redirect( '' ); -} - -// check if we know who you declare to be -$known_user = $_COOKIE[ COOKIE_WIKI_USERNAME ]; - -template( 'header' ); +$page = new CronosHomepage(); + +$page->printHeader(); ?>

Wikimedia CH Cronos

- + isEventsPageTitleKnown() ): ?>
- + hasSaved() ): ?>

getEventsPageURL(), + $page->getEventsPageTitle(), + __( "Show on wiki" ), ) ?>

- + isUserUnknown() ): ?> - + hasSaved() ): ?>

getAnnouncedUsername() ) ) ?>

- /> - + getPOST( 'event_title' ) ) ?> /> +
- +
- +

printFooter(); diff --git a/www/static/cronos.js b/www/static/cronos.js index 34e2628..d53dd26 100644 --- a/www/static/cronos.js +++ b/www/static/cronos.js @@ -1,24 +1,34 @@ ( function() { + document.addEventListener( 'DOMContentLoaded', function() { // initialize date pickers ( function() { var elems = document.querySelectorAll( '.datepicker' ); var instances = M.Datepicker.init( elems, { format: 'yyyy-mm-dd', firstDay: 1, onSelect: function( date ) { } } ); + + // avoid to select an end date before the start date + var start = M.Datepicker.getInstance( document.getElementById( 'event-date-start' ) ); + var stop = M.Datepicker.getInstance( document.getElementById( 'event-date-end' ) ); + start.options.onSelect = function( date ) { + stop.options.minDate = date; + }; } )(); // initialize time pickers ( function() { var elems = document.querySelectorAll( '.timepicker' ); var instances = M.Timepicker.init( elems, { twelveHour: false, } ); } )(); } ); + } )(); +