Extension talk:Flattr

From mediawiki.org
Latest comment: 13 years ago by Dedlfix in topic proposal of version 1.0

Annotations to version 0.7[edit]

  • Concering:
// Check sanity on $wgFlattrForceUserid
if (!isset($wgFlattrForceUserid) || $wgFlattrForceUserid != true || $wgFlattrForceUserid != false) {
  $wgFlattrForceUserid = false;
}

No matter what value (and type of this value) you compare to both true or false, it always will result in one true and one false, so the result is always true. If you want a comparision that assures the type is boolean you have to use a type-safe comparion operator === or !==. But what are you afraid of to make such a check? Just use

$wgFlattrForceUserid = !empty($wgFlattrForceUserid);

instead of the code above to force a boolean value. empty() results to true for both not existing variables and values evaluating to false, so the negation is necessary.

  • The following is a useless use of else.
if (isset($args['url']) && !isset($args['uid'])) {
  return ...;
} else { ... }

Because if the condition ist true you will leave the current scope, so you don't need to protect the otherwise-code from execution with an else block encapsulation.

  • Don't style errors yourself. There is a rule for .error in a common CSS file, so you only need a class="error".
  • A wrong value for language should generate an error message, not change silently to en_GB. Or even better: implement both an error message and a forceLanguage option (for single language wikis).
  • It doesn't cost a lot of execution time but it's not necessary to initialize $out if you unconditionally assign a value to it just in the next line.
$out = "";
$out .= "<script type=\"text/javascript\">" . PHP_EOL;

is the same but longer as

$out = '<script type="text/javascript">' . PHP_EOL;
  • If you use empty() to ensure $wgFlattrForceUserid is boolean you can shorten the next line a bit
if (isset($wgFlattrUserid) && $wgFlattrUserid > 0 && (true === $wgFlattrForceUserid  || !isset($args['uid']))) {
  • Do not apply htmlspecialchars() to input values. Typically you will operate in your script with raw values. Apply it just before or while creating the output. If you always place htmlspecialchars() to the output you won't miss lines like this:
$out .= "var flattr_uid = '" . $wgFlattrUserid . "';" . PHP_EOL;

Maybe you trust the admin that the content of $wgFlattrUserid is safe, but I wouldn't rely on it.

In addition, Javascript as content of <script> is not HTML, so you have to escape regarding the JS rules not the HTML rules. Quote from the HTML4.01 spec. [1]: “Although the STYLE and SCRIPT elements use CDATA for their data model, for these elements, CDATA must be handled differently by user agents. Markup and entities must be treated as raw text and passed to the application as is.” - Due to this rule htmlspecialchars() applied to a URL containing an & in a JS context results in the string &amp; and no HTML parser will resolve this to a raw &, so the resulting URL ist broken.

Unfortunately there is no function to escape values for JS strings in PHP, but you can use this one:

function wgFlattrJsEscape($value) {
  return strtr((string)$value, array(
    "'"     => '\\\,
    '"'     => '\"',
    '\\'    => '\\\\',
    "\n"    => '\n',
    "\r"    => '\r',
    "\t"    => '\t',
    chr(12) => '\f',
    chr(11) => '\v',
    chr(8)  => '\b',
    '</'    => '\u003c\u002F',
  ));
}
  • And a copy&paste error:
if (isset($args['url'])) {
  $out .= "var flattr_tag = '" . $args['url'] . "';" . PHP_EOL;
}

Should be var flattr_url instead of ..._tag.

proposal of version 0.8[edit]

obsoleted by 0.9

Changes since 0.7

  • Fixed some security issues (inappropriate escaping)
  • Added some configuration variables

proposal of version 0.9[edit]

obsoleted by 0.9

Changes since 0.8

  • Added more configuration variables.
  • Added static flattr button

proposal of version 1.0[edit]

Changes since 1.0β2

  • Buttons didn't work if content was not rendered but cached

Changes since 1.0β1

  • Added option to auto-insert Flattr buttons before or after main content
  • Changed the extension init code (using a different init hook)
  • Bugfix: sidebar didn't work without any flattr buttons in the main content

Changes since 0.9

  • Now uses Flattr API 0.5.0
  • Added Sidebar option
  • Rewritten to a (static) class instead of a bunch of functions
  • Changed $wgFlattr(Forced)Userid to $wgFlattr(Forced)Uid and $wgFlattrHide to $wgFlattrHidden to harmonize with names used by Flattr API
  • Added short description as code comment

Installation[edit]

New configuration options:

  • $wgFlattrUid - nothing new (but renamed)
  • $wgFlattrForceUid - nothing new (but renamed)
  • $wgFlattrXXX - default configuration value
  • $wgFlattrForceXXX - always use the default value
  • instead XXX use Url, Title, Description, Language, Category, Tags, or Button
  • $wgFlattrHidden - set to any value other than 0, '0', , false to always create a hidden "thing"; i.e. $wgFlattrHide = true;
  • See Flattr API for the meaning of these values
  • For both $wgFlattrButton and the attribute button you can use both or one of compact and static in any combination, i.e. 'static', 'compact', 'static compact', 'compact-static', even 'staticompact' will work.
  • For a static button use the flattr.com-URL for an already submitted thing, i.e. https://flattr.com/thing/0815/Name

--dedlfix 11:10, 23 August 2010 (UTC)Reply

Flattr.php[edit]

<?php
/*
Global configuration variables
------------------------------
All variables are optional. (The values here are examples for usage.)
$wgFlattrSidebar = true; // to show Flattr button in the sidebar
$wgFlattrBeforeContent = true; // to show a Flattr button before page's main content
$wgFlattrAfterContent = true; // to show a Flattr button after page's main content
$wgFlattrLibProto = 'https'; // to use HTTPS to load Flattr's API libs

See http://www.mediawiki.org/wiki/Extension:Flattr for a more verbose description.
$wgFlattrUid = '123456789'; // your Flattr UserID or username
$wgFlattrUrl = 'http://example.com/my/thing'; // defaults to current page's url
$wgFlattrTitle = 'at least 5 chars';
$wgFlattrDescription = 'at least 5 chars';
$wgFlattrLanguage = 'en_GB'; // lang code of your content, see begin of class
$wgFlattrCategory = 'text'; // category of your content, see begin of class
$wgFlattrTags = 'tag1,tag2'; // comma separated list
$wgFlattrButton = 'static-compact'; // appearence of button static and/or compact
$wgFlattrHidden = true; // hide thing from Flattr's lists
For all variables (except of $wgFlattrHidden) there is a $wgFlattrForce...
to coerce the use of the default value, i.e. $wgFlattrForceUid = true;

Syntax
------
See http://www.mediawiki.org/wiki/Extension:Flattr for details.
<flattr uid="…" url="…" title="…" description="…" language="…" category="…" tags="…" button="…" hidden="…" />
Static button:
	Use Flattr's url of the thing, like http://flattr.com/thing/1234/title. 
	Only url, title, and button are considered.
For both submitted and autosubmit version use thing's url: http://your.site.example.com/my/thing
Submitted:
  Omit uid. Only both uid and button are considered.
Autosubmit will consider/need all values.

Sidebar
-------
- Create MediaWiki:{content of flattr-sidebar-box} (default: MediaWiki:Flattr)
to change/translate the heading of the Flattr box
- MediaWiki:Flattr-sidebar-box is the name of the box, also shown if
MediaWiki:{content of flattr-sidebar-box} is not available
- Insert Flattr button code or any other wiki syntax into MediaWiki:Flattr-sidebar-content
- You maybe have to adjust your skin specific CSS

Before/after Content
--------------------
Create MediaWiki:Flattr-before-content or MediaWiki:Flattr-after-content and insert Flattr button's code.
You can surround it with other elements like <div class="Flattr"><flattr /></div>.
Don't use a class named FlattrButton because this name is reserved by the Flattr-API.
This functionality is limited to Namespaces listed in $wgContentNamespaces.
*/

if ( !defined( 'MEDIAWIKI' ) ) {
	echo( "This is an extension to the MediaWiki package and cannot be run standalone.\n" );
	die( -1 );
}

// Extension credits that will show up on Special:Version
$wgExtensionCredits['parserhook'][] = array(
	'path'           => __FILE__,
	'name'           => 'Flattr',
	'version'        => '1.0β3',
	'author'         => 'Christian Riesen, dedlfix',
	'url'            => 'http://www.mediawiki.org/wiki/Extension:Flattr',
	'descriptionmsg' => 'flattr-desc',
);

$wgExtensionMessagesFiles['Flattr'] = dirname( __FILE__ ) . '/' . 'Flattr.i18n.php';
$wgExtensionFunctions[] = 'Flattr::Init';

class Flattr {
	// used to point to the static flattr buttons
	const FLATTR_EXTENSION_PATH = '/extensions/Flattr'; // with or without leading/trailing slashes

	// allowed values for both category and language
	private static $categories = array( 'text', 'images', 'video', 'audio', 'software', 'rest' );
	private static $languages = array( 'sq_AL', 'be_BY', 'bg_BG', 'ca_ES', 'zh_CN', 'hr_HR', 'cs_CZ', 'da_DK',
		'nl_NL', 'en_GB', 'et_EE', 'fi_FI', 'fr_FR', 'de_DE', 'el_GR', 'iw_IL', 'hi_IN', 'hu_HU', 'is_IS', 'in_ID',
		'ga_IE', 'it_IT', 'ja_JP', 'ko_KR', 'lv_LV', 'lt_LT', 'mk_MK', 'ms_MY', 'mt_MT', 'no_NO', 'pl_PL', 'pt_PT',
		'ro_RO', 'ru_RU', 'sr_RS', 'sk_SK', 'sl_SI', 'es_ES', 'sv_SE', 'th_TH', 'tr_TR', 'uk_UA', 'vi_VN' );

	private static $needsToRenderHeadItem = false;
	// Flattr lib loading protocol
	private static $proto = 'http';

	// common initialization
	public static function Init() {
		global $wgHooks, $wgParser, $wgProto,
			$wgFlattrSidebar, $wgFlattrLibProto;
		// setup parser hook
		$wgParser->setHook( 'flattr', __CLASS__ . '::Render' );

		// sidebar hook
		if ( !empty( $wgFlattrSidebar ) )
			$wgHooks['SkinBuildSidebar'][] = __CLASS__ . '::Sidebar';

		// hook for before/after content and for JS head code
		$wgHooks['BeforePageDisplay'][] = __CLASS__ . '::BeforePageDisplay';

		// determine protocol (https/http) for loading Flattr lib
		self::$proto = empty( $wgFlattrLibProto ) ? $wgProto : ( strtolower( $wgFlattrLibProto ) == 'https' ? 'https' : 'http' );
		
		return true;
	}

	// create sidebar code for Flattr box
	public static function Sidebar( $skin, &$bar ) {
		global $wgOut;
		$boxName = wfMsg( 'flattr-sidebar-box' );
		if ( isset( $bar[$boxName] ) ) {
			$bar[$boxName] = wfMsgExt( 'flattr-sidebar-content', 'parseinline' );
			if ( strpos( $bar[$boxName], '<a class="FlattrButton"' ) !== false )
				$wgOut->addHeadItem( 'flattr', self::headItem() );
		}
		return true;
	}

	// create code for Flattr button(s) before/after the main content
	public static function BeforePageDisplay( OutputPage $out, $sk ) {
		global $wgRequest, $wgTitle, $wgContentNamespaces, $wgFlattrBeforeContent, $wgFlattrAfterContent;
		// action == empty; only content namespaces
		if ( !$wgRequest->getBool('action') and  in_array( $wgTitle->mNamespace, $wgContentNamespaces ) ) {

			if ( !empty( $wgFlattrBeforeContent ) )
				$out->prependHTML( wfMsgExt( 'flattr-before-content', 'parseinline' ) );

			if ( !empty( $wgFlattrAfterContent ) )
				$out->addHTML( wfMsgExt( 'flattr-after-content', 'parseinline' ) );
		}
		// for chached content the Render method is not called; checking the content for FlattrButtons
		if ( self::$needsToRenderHeadItem or strpos( $out->mBodytext, '<a class="FlattrButton"' ) !== false )
			$out->addHeadItem( 'flattr', self::headItem() );
		return true;
	}

	private static function headItem(  ) {
		return sprintf( '<script type="text/javascript">/* <![CDATA[ */ ' .
			'addOnloadHook(function() { importScriptURI("%s://api.flattr.com/js/0.5.0/load.js?mode=auto"); }); /* ]]> */</script>%2$s' .
			'<style type="text/css"> .FlattrButton {	display: none; } </style>%2$s' , self::$proto,  "\n" );
	}
	
	private static function defaults( $args, $key, $config, $configForce, $default = false ) {
		return ( !isset( $args[$key] ) or !empty( $configForce ) ) // not set or forced
			? ( isset( $config ) ? (string)$config : $default ) // try to get default value
			: $args[$key]; // otherwise the given value
	}
	
	public static function Render( $input, $argsRaw, $parser ) {
		global
			$wgFlattrUid, $wgFlattrForceUid,
			$wgFlattrUrl, $wgFlattrForceUrl,
			$wgFlattrTitle, $wgFlattrForceTitle,
			$wgFlattrDescription, $wgFlattrForceDescription,
			$wgFlattrLanguage, $wgFlattrForceLanguage,
			$wgFlattrCategory, $wgFlattrForceCategory,
			$wgFlattrTags, $wgFlattrForceTags,
			$wgFlattrButton, $wgFlattrForceButton,
			$wgFlattrHidden;
	 
		$args = array();
		foreach ( $argsRaw as $name => $value )
			$args[$name] = $parser->replaceVariables( $value );

		// gather values (false if no applicable value can be found)
		$uid = self::defaults( $args, 'uid', $wgFlattrUid, $wgFlattrForceUid );
		$url = self::defaults( $args, 'url', $wgFlattrUrl, $wgFlattrForceUrl );
		$title = self::defaults( $args, 'title', $wgFlattrTitle, $wgFlattrForceTitle );
		$description = self::defaults( $args, 'description', $wgFlattrDescription, $wgFlattrForceDescription );
		$language = self::defaults( $args, 'language', $wgFlattrLanguage, $wgFlattrForceLanguage );
		$category = self::defaults( $args, 'category', $wgFlattrCategory, $wgFlattrForceCategory );
		$tags = self::defaults( $args, 'tags', $wgFlattrTags, $wgFlattrForceTags );
		$button = self::defaults( $args, 'button', $wgFlattrButton, $wgFlattrForceButton, '' );
		$hidden = !empty( $args['hidden'] ) or !empty( $wgFlattrHidden );

		// Three versions: static button, already submitted, and auto submit

		// Version: static button
		if ( stripos( $button, 'static' ) !== false ) {
			if ( !$url )
				return '<div class="error">flattr error: missing url</div>';

			return sprintf('<a href="%s"><img border="0" title="%s" alt="Flattr this" src="%s/%s/%s" /></a>',
				htmlspecialchars( $url ),
				$title ? htmlspecialchars( $title ) : 'Flattr this',
				$GLOBALS['wgScriptPath'],
				trim( self::FLATTR_EXTENSION_PATH, '/' ),
				stripos( $button, 'compact' ) !== false ? 'flattr-100x17.png' : 'flattr-50x60.png' );
		}
		
		// Version: already submitted
		if ( !$uid ) {
			// Without UID doing it the direct way. A thing must have been already submitted.
			if ( !$url )
				$url = $parser->mTitle->getFullURL();
			self::$needsToRenderHeadItem = true;
			$rev = stripos( $button, 'compact' ) !== false ? ' rev="flattr;button:compact"' : '';
			return sprintf( '<a class="FlattrButton" href="%s"%s></a>', htmlspecialchars( $url ), $rev );
		}
	 
		// Version: auto submit

		// functional checks
		$errors = array();
	 
		if ( !$uid )
			$errors[] = 'flattr error: missing user id';

		if ( !$url )
			$url = $parser->mTitle->getFullURL();
		
		if ( !$title )
			$errors[] = 'flattr error: missing title';
	 
		if ( !$description )
			$errors[] = 'flattr error: missing description';

		if ( !in_array( $category, self::$categories ) )
			$errors[] = 'flattr error: missing or invalid category';
	 
		if ( !in_array( $language, self::$languages ) )
			$errors[] = 'flattr error: missing or invalid language';
	 
		if ( $errors ) {
			$out = "<ul>\n";
			foreach ( $errors as $error )
				$out .= '<li class="error">' . $error . "</li>\n";
			return $out . "</ul>\n";
		}

		// checks passed, build rev values and render
		self::$needsToRenderHeadItem = true;
		$rev = array( 'flattr' );

		// uid
		if ( $uid )
			$rev[] = 'uid:' . self::flattrEscape( $uid );
		
		// category
		$rev[] = 'category:' . $category;

		// tags in comma separated list
		if ( $tags )
			$rev[] = 'tags:' . self::flattrEscape( $tags );
	 
		// button style
		if ( stripos( $button, 'compact' ) !== false )
			$rev[] = 'button:compact';
	 
		// hide thing
		if ( $hidden )
			$rev[] = 'hidden:1';
	 
		return sprintf( '<a class="FlattrButton" href="%s" title="%s" lang="%s" rev="%s">%s</a>',
			htmlspecialchars( $url ),
			htmlspecialchars( strip_tags( $title ) ),
			htmlspecialchars( $language ),
			htmlspecialchars( implode( ';', $rev ) ),
			htmlspecialchars( strip_tags( $description ) ) );
	}
	
	// both ; and : are separation chars in the rev attribute defined by Flattr API
	private static function flattrEscape( $s ) {
		return str_replace( array( ';', ':' ), ',', $s );
	}
}

Flattr.i18n.php[edit]

<?php

/* Internationalisation file for Flattr extension
*/

$messages = array();

$messages['en'] = array(
	'flattr-desc' => 'Allows safe placements of flattr buttons on your wiki.',
	'flattr-sidebar-box' => 'Flattr',
	'flattr-sidebar-content' => '<flattr />',
	'flattr-before-content' => '<div class="Flattr"><flattr /></div>',
	'flattr-after-content' => '<div class="Flattr"><flattr /></div>',
);

/** Message documentation (Message documentation)
 */
$messages['qqq'] = array(
	'flattr-desc' => 'Short description of the Flattr extension, shown on [[Special:Version]].',
	'flattr-sidebar-box' => 'Name of the sidebar box the Flattr-button should be placed.',
	'flattr-sidebar-content' => 'Content of the Flattr sidebar box.',
	'flattr-before-content' => 'Content to show before page\'s main content',
	'flattr-after-content' => 'Content to show after page\'s main content',
);

/** German (Deutsch)
 */
$messages['de'] = array(
	'flattr-desc' => 'Erlaubt die sichere Platzierung von Flattr-Buttons im Wiki',
	'flattr-sidebar-box' => 'Flattr',
	'flattr-sidebar-content' => '<flattr />',
	'flattr-before-content' => '<div class="Flattr"><flattr /></div>',
	'flattr-after-content' => '<div class="Flattr"><flattr /></div>',
);