diff --git a/include/class-Category.php b/include/class-Category.php new file mode 100644 index 0000000..7d3fbd1 --- /dev/null +++ b/include/class-Category.php @@ -0,0 +1,187 @@ +. + +/** + * Describes a Cronos Calendar Category + */ +class Category { + + /** + * Category UID + */ + private $uid; + + /** + * Category name + */ + private $name; + + /** + * Category filename on Wikimedia Commons without File: prefix + */ + private $filename; + + /** + * URL in sprintf format + * + * The %d argument can be replaced with the width in pixels. + */ + private $urlFormat; + + /** + * All the known categories + */ + private static $all = []; + + /** + * All the known aliases + * + * Associative array of starting Category UID and destination Category UID + */ + private static $aliases = []; + + /** + * Costructor + * + * @param string $uid User identifier + * @param string $name Category name + * @param string $filename File name on Wikimedia Commons + * @param string $url_format URL in sprintf format with '%d' that can be sobstituted with the width in pixels + */ + public function __construct( $uid, $name, $filename, $url_format ) { + $this->uid = $uid; + $this->name = $name; + $this->filename = $filename; + $this->urlFormat = $url_format; + } + + /** + * Get the Category UID + * + * @return string + */ + public function getUID() { + return $this->uid; + } + + /** + * Get the Category Name + * + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * Get the Category filename + * + * @return string + */ + public function getFilename() { + return $this->filename; + } + + /** + * Get Commons URL + * + * @return string + */ + public function getCommonsURL() { + return sprintf( + 'https://commons.wikimedia.org/wiki/%s', + str_replace( ' ', '_', $this->filename ) + ); + } + + /** + * Get the image URL + * + * @param $size int You can set the width in pixels + * @return string + */ + public function getImageURL( $size = 64 ) { + return sprintf( + $this->urlFormat, + $size + ); + } + + /** + * Add a new Category + * + * @param string $uid User identifier + * @param string $name Category name + * @param string $filename File name on Wikimedia Commons + * @param string $url_format URL in sprintf format with '%d' that can be sobstituted with the width in pixels + */ + public static function add( $uid, $name, $filename, $url_format ) { + self::$all[ $uid ] = new self( $uid, $name, $filename, $url_format ); + } + + /** + * Add a new Category about "Bla bla initiatives" + * + * @param string $uid User identifier + * @param string $name Category name + * @param string $filename File name on Wikimedia Commons + * @param string $url_format URL in sprintf format with '%d' that can be sobstituted with the width in pixels + */ + public static function addInitiatives( $uid, $name, $filename, $url_format ) { + + // from 'bla bla' + // to 'bla bla initiatives' + $name = sprintf( + __( "%s initiatives" ), + $name + ); + + self::add( $uid, $name, $filename, $url_format ); + } + + /** + * Add a Category alias + * + * @param $from string Category UID to start from + * @param $to string Category UID to start to + */ + public static function addAliasFromTO( $from, $to ) { + self::$aliases[ $from ] = $to; + } + + /** + * Get all the known categories + * + * @return array + */ + public static function all() { + return self::$all; + } + + /** + * Find a Category by UID + * + * @return Category|false + */ + public static function find( $uid ) { + + // eventually replace with its existing alias + $uid = self::$aliases[ $uid ] ?? $uid; + + // return the existing Category or false + return self::$all[ $uid ] ?? false; + } +} diff --git a/include/class-CronosHomepage.php b/include/class-CronosHomepage.php index ca98288..e6df2a3 100644 --- a/include/class-CronosHomepage.php +++ b/include/class-CronosHomepage.php @@ -1,424 +1,426 @@ . // 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 ] ) ) { + if( isset( $_POST[ $key ] ) && 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' ); + $event_category = $this->getPOST( 'event_category' ); // 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" . + "|title = $event_title\n" . + "|when = $event_date_start $event_time_start\n" . + "|end = $event_date_end $event_time_end\n" . + "|url = $event_url\n" . + "|category = $event_category\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", + 'summary' => "New events page: $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 48e5c84..8c25185 100644 --- a/load-post.php +++ b/load-post.php @@ -1,33 +1,47 @@ . // 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 some dummy classes require ABSPATH . '/include/class-CronosHomepage.php'; +require ABSPATH . '/include/class-Category.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', ] ); + +// register some dummy categories in order of appearance +Category::addInitiatives( 'com', __( "Community" ), 'Wikimedia Community Logo.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Wikimedia_Community_Logo.svg/%dpx-Wikimedia_Community_Logo.svg.png' ); +Category::addInitiatives( 'dat', __( "Wikidata" ), 'Wikidata Favicon color.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/Wikidata_Favicon_color.svg/%spx-Wikidata_Favicon_color.svg.png' ); +Category::add( 'edu', __( "Wikimedia Education Program" ), 'WikipediaEduBelow.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/WikipediaEduBelow.svg/%dpx-WikipediaEduBelow.svg.png' ); +Category::addInitiatives( 'libre', __( "Free Software and Open Source" ), 'Heckert GNU white.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Heckert_GNU_white.svg/%dpx-Heckert_GNU_white.svg.png' ); +Category::add( 'osm', "OpenStreetMap", 'Openstreetmap logo.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Openstreetmap_logo.svg/%dpx-Openstreetmap_logo.svg.png' ); +Category::add( 'glam', __( "Wikimedia GLAM Program" ), 'GLAM logo.png', 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/GLAM_logo.png/%dpx-GLAM_logo.png' ); +Category::addInitiatives( 'wmch', __( "Wikimedia CH" ), 'WikimediaCHLogo.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fe/WikimediaCHLogo.svg/%dpx-WikimediaCHLogo.svg.png' ); +Category::addInitiatives( 'wmf', __( "Wikimedia Foundation" ), 'Wikimedia-logo black.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Wikimedia-logo_black.svg/%dpx-Wikimedia-logo_black.svg.png' ); + +// this should be an alias of libre +// Category::addInitiatives( 'osi', __( "Open Source" ), 'Opensource.svg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/42/Opensource.svg/%dpx-Opensource.svg.png' ); diff --git a/template/form-login.php b/template/form-login.php index 1951f0c..4db294e 100644 --- a/template/form-login.php +++ b/template/form-login.php @@ -1,33 +1,33 @@ . ?>

-

+

diff --git a/www/credits.php b/www/credits.php new file mode 100644 index 0000000..f868be9 --- /dev/null +++ b/www/credits.php @@ -0,0 +1,62 @@ +. + +// load the configuration file and autoload classes +require 'load.php'; + +// enqueue default stylesheet +enqueue_js( 'cronos' ); + +template( 'header' ); +?> +
+

Wikimedia CH Cronos —

+ +
+

+

+ +
+ +
+

+

+
getUID(),
+						$category->getName(),
+						$category->getFilename()
+					);
+				}
+
+			?>
+
+ +
+ +

+
+ +. // load the configuration file and autoload classes require 'load.php'; $page = new CronosHomepage(); $page->printHeader(); ?>

Wikimedia CH Cronos

isEventsPageTitleKnown() ): ?>
hasSaved() ): ?>

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

isUserUnknown() ): ?> hasSaved() ): ?>

getAnnouncedUsername() ) ) ?>

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

+

+

printFooter(); diff --git a/www/static/cronos.js b/www/static/cronos.js index d53dd26..5a10885 100644 --- a/www/static/cronos.js +++ b/www/static/cronos.js @@ -1,34 +1,40 @@ ( 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, } ); } )(); + + // initialize the select boxes + ( function() { + var elems = document.querySelectorAll( 'select' ); + var instances = M.FormSelect.init( elems ); + } )(); } ); } )();