code:
/**
 * editable_selects.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://tinymce.moxiecode.com/license
 * Contributing: http://tinymce.moxiecode.com/contributing
 */

var TinyMCE_EditableSelects = {
	editSelectElm : null,

	init : function() {
		var nl = document.getElementsByTagName("select"), i, d = document, o;

		for (i=0; i<nl.length; i++) {
			if (nl[i].className.indexOf('mceEditableSelect') != -1) {
				o = new Option('(value)', '__mce_add_custom__');

				o.className = 'mceAddSelectValue';

				nl[i].options[nl[i].options.length] = o;
				nl[i].onchange = TinyMCE_EditableSelects.onChangeEditableSelect;
			}
		}
	},

	onChangeEditableSelect : function(e) {
		var d = document, ne, se = window.event ? window.event.srcElement : e.target;

		if (se.options[se.selectedIndex].value == '__mce_add_custom__') {
			ne = d.createElement("input");
			ne.id = se.id + "_custom";
			ne.name = se.name + "_custom";
			ne.type = "text";

			ne.style.width = se.offsetWidth + 'px';
			se.parentNode.insertBefore(ne, se);
			se.style.display = 'none';
			ne.focus();
			ne.onblur = TinyMCE_EditableSelects.onBlurEditableSelectInput;
			ne.onkeydown = TinyMCE_EditableSelects.onKeyDown;
			TinyMCE_EditableSelects.editSelectElm = se;
		}
	},

	onBlurEditableSelectInput : function() {
		var se = TinyMCE_EditableSelects.editSelectElm;

		if (se) {
			if (se.previousSibling.value != '') {
				addSelectValue(document.forms[0], se.id, se.previousSibling.value, se.previousSibling.value);
				selectByValue(document.forms[0], se.id, se.previousSibling.value);
			} else
				selectByValue(document.forms[0], se.id, '');

			se.style.display = 'inline';
			se.parentNode.removeChild(se.previousSibling);
			TinyMCE_EditableSelects.editSelectElm = null;
		}
	},

	onKeyDown : function(e) {
		e = e || window.event;

		if (e.keyCode == 13)
			TinyMCE_EditableSelects.onBlurEditableSelectInput();
	}
};

license: mit


code:
/**
 * form_utils.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://tinymce.moxiecode.com/license
 * Contributing: http://tinymce.moxiecode.com/contributing
 */

var themeBaseURL = tinyMCEPopup.editor.baseURI.toAbsolute('themes/' + tinyMCEPopup.getParam("theme"));

function getColorPickerHTML(id, target_form_element) {
	var h = "", dom = tinyMCEPopup.dom;

	if (label = dom.select('label[for=' + target_form_element + ']')[0]) {
		label.id = label.id || dom.uniqueId();
	}

	h += '';
	h += ' ' + tinyMCEPopup.getLang('browse') + '';

	return h;
}

function updateColor(img_id, form_element_id) {
	document.getElementById(img_id).style.backgroundColor = document.forms[0].elements[form_element_id].value;
}

function setBrowserDisabled(id, state) {
	var img = document.getElementById(id);
	var lnk = document.getElementById(id + "_link");

	if (lnk) {
		if (state) {
			lnk.setAttribute("realhref", lnk.getAttribute("href"));
			lnk.removeAttribute("href");
			tinyMCEPopup.dom.addClass(img, 'disabled');
		} else {
			if (lnk.getAttribute("realhref"))
				lnk.setAttribute("href", lnk.getAttribute("realhref"));

			tinyMCEPopup.dom.removeClass(img, 'disabled');
		}
	}
}

function getBrowserHTML(id, target_form_element, type, prefix) {
	var option = prefix + "_" + type + "_browser_callback", cb, html;

	cb = tinyMCEPopup.getParam(option, tinyMCEPopup.getParam("file_browser_callback"));

	if (!cb)
		return "";

	html = "";
	html += '';
	html += ' ';

	return html;
}

function openBrowser(img_id, target_form_element, type, option) {
	var img = document.getElementById(img_id);

	if (img.className != "mceButtonDisabled")
		tinyMCEPopup.openBrowser(target_form_element, type, option);
}

function selectByValue(form_obj, field_name, value, add_custom, ignore_case) {
	if (!form_obj || !form_obj.elements[field_name])
		return;

	if (!value)
		value = "";

	var sel = form_obj.elements[field_name];

	var found = false;
	for (var i=0; i<sel.options.length; i++) {
		var option = sel.options[i];

		if (option.value == value || (ignore_case && option.value.toLowerCase() == value.toLowerCase())) {
			option.selected = true;
			found = true;
		} else
			option.selected = false;
	}

	if (!found && add_custom && value != '') {
		var option = new Option(value, value);
		option.selected = true;
		sel.options[sel.options.length] = option;
		sel.selectedIndex = sel.options.length - 1;
	}

	return found;
}

function getSelectValue(form_obj, field_name) {
	var elm = form_obj.elements[field_name];

	if (elm == null || elm.options == null || elm.selectedIndex === -1)
		return "";

	return elm.options[elm.selectedIndex].value;
}

function addSelectValue(form_obj, field_name, name, value) {
	var s = form_obj.elements[field_name];
	var o = new Option(name, value);
	s.options[s.options.length] = o;
}

function addClassesToList(list_id, specific_option) {
	// Setup class droplist
	var styleSelectElm = document.getElementById(list_id);
	var styles = tinyMCEPopup.getParam('theme_advanced_styles', false);
	styles = tinyMCEPopup.getParam(specific_option, styles);

	if (styles) {
		var stylesAr = styles.split(';');

		for (var i=0; i


code:
/**
 * mctabs.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://tinymce.moxiecode.com/license
 * Contributing: http://tinymce.moxiecode.com/contributing
 */

function MCTabs() {
	this.settings = [];
	this.onChange = tinyMCEPopup.editor.windowManager.createInstance('tinymce.util.Dispatcher');
};

MCTabs.prototype.init = function(settings) {
	this.settings = settings;
};

MCTabs.prototype.getParam = function(name, default_value) {
	var value = null;

	value = (typeof(this.settings[name]) == "undefined") ? default_value : this.settings[name];

	// Fix bool values
	if (value == "true" || value == "false")
		return (value == "true");

	return value;
};

MCTabs.prototype.showTab =function(tab){
	tab.className = 'current';
	tab.setAttribute("aria-selected", true);
	tab.setAttribute("aria-expanded", true);
	tab.tabIndex = 0;
};

MCTabs.prototype.hideTab =function(tab){
	var t=this;

	tab.className = '';
	tab.setAttribute("aria-selected", false);
	tab.setAttribute("aria-expanded", false);
	tab.tabIndex = -1;
};

MCTabs.prototype.showPanel = function(panel) {
	panel.className = 'current';
	panel.setAttribute("aria-hidden", false);
};

MCTabs.prototype.hidePanel = function(panel) {
	panel.className = 'panel';
	panel.setAttribute("aria-hidden", true);
};

MCTabs.prototype.getPanelForTab = function(tabElm) {
	return tinyMCEPopup.dom.getAttrib(tabElm, "aria-controls");
};

MCTabs.prototype.displayTab = function(tab_id, panel_id, avoid_focus) {
	var panelElm, panelContainerElm, tabElm, tabContainerElm, selectionClass, nodes, i, t = this;

	tabElm = document.getElementById(tab_id);

	if (panel_id === undefined) {
		panel_id = t.getPanelForTab(tabElm);
	}

	panelElm= document.getElementById(panel_id);
	panelContainerElm = panelElm ? panelElm.parentNode : null;
	tabContainerElm = tabElm ? tabElm.parentNode : null;
	selectionClass = t.getParam('selection_class', 'current');

	if (tabElm && tabContainerElm) {
		nodes = tabContainerElm.childNodes;

		// Hide all other tabs
		for (i = 0; i < nodes.length; i++) {
			if (nodes[i].nodeName == "LI") {
				t.hideTab(nodes[i]);
			}
		}

		// Show selected tab
		t.showTab(tabElm);
	}

	if (panelElm && panelContainerElm) {
		nodes = panelContainerElm.childNodes;

		// Hide all other panels
		for (i = 0; i < nodes.length; i++) {
			if (nodes[i].nodeName == "DIV")
				t.hidePanel(nodes[i]);
		}

		if (!avoid_focus) {
			tabElm.focus();
		}

		// Show selected panel
		t.showPanel(panelElm);
	}
};

MCTabs.prototype.getAnchor = function() {
	var pos, url = document.location.href;

	if ((pos = url.lastIndexOf('#')) != -1)
		return url.substring(pos + 1);

	return "";
};

//Global instance
var mcTabs = new MCTabs();

tinyMCEPopup.onInit.add(function() {
	var tinymce = tinyMCEPopup.getWin().tinymce, dom = tinyMCEPopup.dom, each = tinymce.each;

	each(dom.select('div.tabs'), function(tabContainerElm) {
		var keyNav;

		dom.setAttrib(tabContainerElm, "role", "tablist");

		var items = tinyMCEPopup.dom.select('li', tabContainerElm);
		var action = function(id) {
			mcTabs.displayTab(id, mcTabs.getPanelForTab(id));
			mcTabs.onChange.dispatch(id);
		};

		each(items, function(item) {
			dom.setAttrib(item, 'role', 'tab');
			dom.bind(item, 'click', function(evt) {
				action(item.id);
			});
		});

		dom.bind(dom.getRoot(), 'keydown', function(evt) {
			if (evt.keyCode === 9 && evt.ctrlKey && !evt.altKey) { // Tab
				keyNav.moveFocus(evt.shiftKey ? -1 : 1);
				tinymce.dom.Event.cancel(evt);
			}
		});

		each(dom.select('a', tabContainerElm), function(a) {
			dom.setAttrib(a, 'tabindex', '-1');
		});

		keyNav = tinyMCEPopup.editor.windowManager.createInstance('tinymce.ui.KeyboardNavigation', {
			root: tabContainerElm,
			items: items,
			onAction: action,
			actOnFocus: true,
			enableLeftRight: true,
			enableUpDown: true
		}, tinyMCEPopup.dom);
	});
});


code:
/**
 * validate.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://tinymce.moxiecode.com/license
 * Contributing: http://tinymce.moxiecode.com/contributing
 */

/**
	// String validation:

	if (!Validator.isEmail('myemail'))
		alert('Invalid email.');

	// Form validation:

	var f = document.forms['myform'];

	if (!Validator.isEmail(f.myemail))
		alert('Invalid email.');
*/

var Validator = {
	isEmail : function(s) {
		return this.test(s, '^[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+@[-!#$%&\'*+\\/0-9=?A-Z^_`a-z{|}~]+\.[-!#$%&\'*+\\./0-9=?A-Z^_`a-z{|}~]+$');
	},

	isAbsUrl : function(s) {
		return this.test(s, '^(news|telnet|nttp|file|http|ftp|https)://[-A-Za-z0-9\\.]+\\/?.*$');
	},

	isSize : function(s) {
		return this.test(s, '^[0-9.]+(%|in|cm|mm|em|ex|pt|pc|px)?$');
	},

	isId : function(s) {
		return this.test(s, '^[A-Za-z_]([A-Za-z0-9_])*$');
	},

	isEmpty : function(s) {
		var nl, i;

		if (s.nodeName == 'SELECT' && s.selectedIndex < 1)
			return true;

		if (s.type == 'checkbox' && !s.checked)
			return true;

		if (s.type == 'radio') {
			for (i=0, nl = s.form.elements; i<nl.length; i++) {
				if (nl[i].type == "radio" && nl[i].name == s.name && nl[i].checked)
					return false;
			}

			return true;
		}

		return new RegExp('^\\s*$').test(s.nodeType == 1 ? s.value : s);
	},

	isNumber : function(s, d) {
		return !isNaN(s.nodeType == 1 ? s.value : s) && (!d || !this.test(s, '^-?[0-9]*\\.[0-9]*$'));
	},

	test : function(s, p) {
		s = s.nodeType == 1 ? s.value : s;

		return s == '' || new RegExp(p).test(s);
	}
};

var AutoValidator = {
	settings : {
		id_cls : 'id',
		int_cls : 'int',
		url_cls : 'url',
		number_cls : 'number',
		email_cls : 'email',
		size_cls : 'size',
		required_cls : 'required',
		invalid_cls : 'invalid',
		min_cls : 'min',
		max_cls : 'max'
	},

	init : function(s) {
		var n;

		for (n in s)
			this.settings[n] = s[n];
	},

	validate : function(f) {
		var i, nl, s = this.settings, c = 0;

		nl = this.tags(f, 'label');
		for (i=0; i<nl.length; i++) {
			this.removeClass(nl[i], s.invalid_cls);
			nl[i].setAttribute('aria-invalid', false);
		}

		c += this.validateElms(f, 'input');
		c += this.validateElms(f, 'select');
		c += this.validateElms(f, 'textarea');

		return c == 3;
	},

	invalidate : function(n) {
		this.mark(n.form, n);
	},

	getErrorMessages : function(f) {
		var nl, i, s = this.settings, field, msg, values, messages = [], ed = tinyMCEPopup.editor;
		nl = this.tags(f, "label");
		for (i=0; i<nl.length; i++) {
			if (this.hasClass(nl[i], s.invalid_cls)) {
				field = document.getElementById(nl[i].getAttribute("for"));
				values = { field: nl[i].textContent };
				if (this.hasClass(field, s.min_cls, true)) {
					message = ed.getLang('invalid_data_min');
					values.min = this.getNum(field, s.min_cls);
				} else if (this.hasClass(field, s.number_cls)) {
					message = ed.getLang('invalid_data_number');
				} else if (this.hasClass(field, s.size_cls)) {
					message = ed.getLang('invalid_data_size');
				} else {
					message = ed.getLang('invalid_data');
				}

				message = message.replace(/{\#([^}]+)\}/g, function(a, b) {
					return values[b] || '{#' + b + '}';
				});
				messages.push(message);
			}
		}
		return messages;
	},

	reset : function(e) {
		var t = ['label', 'input', 'select', 'textarea'];
		var i, j, nl, s = this.settings;

		if (e == null)
			return;

		for (i=0; i<t.length; i++) {
			nl = this.tags(e.form ? e.form : e, t[i]);
			for (j=0; j<nl.length; j++) {
				this.removeClass(nl[j], s.invalid_cls);
				nl[j].setAttribute('aria-invalid', false);
			}
		}
	},

	validateElms : function(f, e) {
		var nl, i, n, s = this.settings, st = true, va = Validator, v;

		nl = this.tags(f, e);
		for (i=0; i<nl.length; i++) {
			n = nl[i];

			this.removeClass(n, s.invalid_cls);

			if (this.hasClass(n, s.required_cls) && va.isEmpty(n))
				st = this.mark(f, n);

			if (this.hasClass(n, s.number_cls) && !va.isNumber(n))
				st = this.mark(f, n);

			if (this.hasClass(n, s.int_cls) && !va.isNumber(n, true))
				st = this.mark(f, n);

			if (this.hasClass(n, s.url_cls) && !va.isAbsUrl(n))
				st = this.mark(f, n);

			if (this.hasClass(n, s.email_cls) && !va.isEmail(n))
				st = this.mark(f, n);

			if (this.hasClass(n, s.size_cls) && !va.isSize(n))
				st = this.mark(f, n);

			if (this.hasClass(n, s.id_cls) && !va.isId(n))
				st = this.mark(f, n);

			if (this.hasClass(n, s.min_cls, true)) {
				v = this.getNum(n, s.min_cls);

				if (isNaN(v) || parseInt(n.value) < parseInt(v))
					st = this.mark(f, n);
			}

			if (this.hasClass(n, s.max_cls, true)) {
				v = this.getNum(n, s.max_cls);

				if (isNaN(v) || parseInt(n.value) > parseInt(v))
					st = this.mark(f, n);
			}
		}

		return st;
	},

	hasClass : function(n, c, d) {
		return new RegExp('\\b' + c + (d ? '[0-9]+' : '') + '\\b', 'g').test(n.className);
	},

	getNum : function(n, c) {
		c = n.className.match(new RegExp('\\b' + c + '([0-9]+)\\b', 'g'))[0];
		c = c.replace(/[^0-9]/g, '');

		return c;
	},

	addClass : function(n, c, b) {
		var o = this.removeClass(n, c);
		n.className = b ? c + (o != '' ? (' ' + o) : '') : (o != '' ? (o + ' ') : '') + c;
	},

	removeClass : function(n, c) {
		c = n.className.replace(new RegExp("(^|\\s+)" + c + "(\\s+|$)"), ' ');
		return n.className = c != ' ' ? c : '';
	},

	tags : function(f, s) {
		return f.getElementsByTagName(s);
	},

	mark : function(f, n) {
		var s = this.settings;

		this.addClass(n, s.invalid_cls);
		n.setAttribute('aria-invalid', 'true');
		this.markLabels(f, n, s.invalid_cls);

		return false;
	},

	markLabels : function(f, n, ic) {
		var nl, i;

		nl = this.tags(f, "label");
		for (i=0; i<nl.length; i++) {
			if (nl[i].getAttribute("for") == n.id || nl[i].htmlFor == n.id)
				this.addClass(nl[i], ic);
		}

		return null;
	}
};


code:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2010-03-01 14:58-0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: hello.php:115 SamplePlugin.php:266
msgid "Hello"
msgstr ""

#: hello.php:117 hello.php:141
#, php-format
msgid "Hello, %s"
msgstr ""

#: hello.php:138
msgid "Hello, stranger!"
msgstr ""

#: hello.php:143
#, php-format
msgid "I have greeted you %d time."
msgid_plural "I have greeted you %d times."
msgstr[0] ""
msgstr[1] ""

#: SamplePlugin.php:266
msgid "A warm greeting"
msgstr ""

#: SamplePlugin.php:277
msgid "A sample plugin to show basics of development for new hackers."
msgstr ""

#: User_greeting_count.php:163
#, php-format
msgid "Could not save new greeting count for %d"
msgstr ""

#: User_greeting_count.php:176
#, php-format
msgid "Could not increment greeting count for %d"
msgstr ""


code:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#. TRANS: Plugin description.
#: TinyMCEPlugin.php:87
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""


code:
# Translation of StatusNet - TinyMCE to Catalan (català)
# Exported from translatewiki.net
#
# Author: Toniher
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:18+0000\n"
"Language-Team: Catalan <https://translatewiki.net/wiki/Portal:ca>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: ca\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Fes servir la biblioteca TinyMCE per permetre l'edició de text ric en el "
"navegador."


code:
# Translation of StatusNet - TinyMCE to German (Deutsch)
# Exported from translatewiki.net
#
# Author: Giftpflanze
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:18+0000\n"
"Language-Team: German <https://translatewiki.net/wiki/Portal:de>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: de\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Benutze die TinyMCE-Bibliothek um Rich-Text-Bearbeitung im Browser zu "
"ermöglichen."


code:
# Translation of StatusNet - TinyMCE to Esperanto (Esperanto)
# Exported from translatewiki.net
#
# Author: Eliovir
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:18+0000\n"
"Language-Team: Esperanto <https://translatewiki.net/wiki/Portal:eo>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: eo\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Uzas la bibliotekon TinyMCE por ebligi redakton de riĉigita teksto en la "
"retumilo."


code:
# Translation of StatusNet - TinyMCE to Spanish (español)
# Exported from translatewiki.net
#
# Author: Translationista
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:18+0000\n"
"Language-Team: Spanish <https://translatewiki.net/wiki/Portal:es>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: es\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Utiliza la biblioteca TinyMCE para permitir la edición de texto enriquecido "
"en el navegador."


code:
# Translation of StatusNet - TinyMCE to French (français)
# Exported from translatewiki.net
#
# Author: Verdy p
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:18+0000\n"
"Language-Team: French <https://translatewiki.net/wiki/Portal:fr>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: fr\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Utiliser la bibliothèque TinyMCE pour permettre la modification de texte "
"enrichi dans le navigateur."


code:
# Translation of StatusNet - TinyMCE to Galician (galego)
# Exported from translatewiki.net
#
# Author: Toliño
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Galician <https://translatewiki.net/wiki/Portal:gl>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: gl\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Usar a biblioteca TinyMCE para permitir a edición de texto enriquecido no "
"navegador."


code:
# Translation of StatusNet - TinyMCE to Hebrew (עברית)
# Exported from translatewiki.net
#
# Author: YaronSh
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Hebrew <https://translatewiki.net/wiki/Portal:he>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: he\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr "שימוש בספרייה TinyMCE כדי לאפשר עריכת טקסט עשיר באמצעות הדפדפן."


code:
# Translation of StatusNet - TinyMCE to Interlingua (interlingua)
# Exported from translatewiki.net
#
# Author: McDutchie
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Interlingua <https://translatewiki.net/wiki/Portal:ia>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: ia\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Usar le bibliotheca TinyMCE pro permitter le modification de texto "
"inricchite in le navigator."


code:
# Translation of StatusNet - TinyMCE to Indonesian (Bahasa Indonesia)
# Exported from translatewiki.net
#
# Author: Farras
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Indonesian <https://translatewiki.net/wiki/Portal:id>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: id\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Gunakan pustaka TinyMCE untuk memungkinkan penyuntingan teks kaya pada "
"peramban."


code:
# Translation of StatusNet - TinyMCE to Japanese (日本語)
# Exported from translatewiki.net
#
# Author: Shirayuki
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"TinyMCE ライブラリーを使用するとブラウザ内でリッチなテキスト編集ができます。"


code:
# Translation of StatusNet - TinyMCE to Macedonian (македонски)
# Exported from translatewiki.net
#
# Author: Bjankuloski06
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Macedonian <https://translatewiki.net/wiki/Portal:mk>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: mk\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n == 1 || n%10 == 1) ? 0 : 1;\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Користи ја библиотеката TinyMCE за уредување со збогатен текст во "
"прелистувачот."


code:
# Translation of StatusNet - TinyMCE to Malay (Bahasa Melayu)
# Exported from translatewiki.net
#
# Author: Anakmalaysia
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Malay <https://translatewiki.net/wiki/Portal:ms>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: ms\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=1; plural=0;\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Gunakan perpustakaan TinyMCE untuk membolehkan penyuntingan teks beraneka "
"(rich text) dalam pelayar ini."


code:
# Translation of StatusNet - TinyMCE to Norwegian Bokmål (norsk (bokmål))
# Exported from translatewiki.net
#
# Author: Nghtwlkr
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Norwegian Bokmål <https://translatewiki.net/wiki/Portal:nb>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: nb\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Bruk TinyMCE-biblioteket for å tillate rik tekstredigering i nettleseren."


code:
# Translation of StatusNet - TinyMCE to Dutch (Nederlands)
# Exported from translatewiki.net
#
# Author: Siebrand
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Dutch <https://translatewiki.net/wiki/Portal:nl>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: nl\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr "TinyMCE gebruiken om WYSIWYG-bewerken in de browser mogelijk te maken."


code:
# Translation of StatusNet - TinyMCE to Portuguese (português)
# Exported from translatewiki.net
#
# Author: Waldir
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Portuguese <https://translatewiki.net/wiki/Portal:pt>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: pt\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr "Use biblioteca TinyMCE para permitir a edição em rich text no browser."


code:
# Translation of StatusNet - TinyMCE to Brazilian Portuguese (português do Brasil)
# Exported from translatewiki.net
#
# Author: Giro720
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:19+0000\n"
"Language-Team: Brazilian Portuguese <https://translatewiki.net/wiki/Portal:"
"pt-br>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: pt-br\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Utilizar a biblioteca TinyMCE para permitir edição em rich text no navegador."


code:
# Translation of StatusNet - TinyMCE to Russian (русский)
# Exported from translatewiki.net
#
# Author: Александр Сигачёв
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:20+0000\n"
"Language-Team: Russian <https://translatewiki.net/wiki/Portal:ru>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: ru\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=3; plural=(n%10 == 1 && n%100 != 11) ? 0 : ( (n%10 >= "
"2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)) ? 1 : 2 );\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Использование библиотеки TinyMCE, для редактирования текста в браузере."


code:
# Translation of StatusNet - TinyMCE to Tagalog (Tagalog)
# Exported from translatewiki.net
#
# Author: AnakngAraw
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:20+0000\n"
"Language-Team: Tagalog <https://translatewiki.net/wiki/Portal:tl>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: tl\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Gamitin ang aklatan ng TinyMCE upang pahintulutan ang pamamatnugot ng "
"mayamang teksto sa pantingin-tingin."


code:
# Translation of StatusNet - TinyMCE to Ukrainian (українська)
# Exported from translatewiki.net
#
# Author: Boogie
# --
# This file is distributed under the same license as the StatusNet package.
#
msgid ""
msgstr ""
"Project-Id-Version: StatusNet - TinyMCE\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2012-06-30 11:07+0000\n"
"PO-Revision-Date: 2012-06-30 11:10:20+0000\n"
"Language-Team: Ukrainian <https://translatewiki.net/wiki/Portal:uk>\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-POT-Import-Date: 2011-06-05 21:52:08+0000\n"
"X-Generator: MediaWiki 1.20alpha (233fc08); Translate 2012-06-21\n"
"X-Translation-Project: translatewiki.net <https://translatewiki.net>\n"
"X-Language-Code: uk\n"
"X-Message-Group: #out-statusnet-plugin-tinymce\n"
"Plural-Forms: nplurals=3; plural=(n%10 == 1 && n%100 != 11) ? 0 : ( (n%10 >= "
"2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)) ? 1 : 2 );\n"

#. TRANS: Plugin description.
msgid "Use TinyMCE library to allow rich text editing in the browser."
msgstr ""
"Використання бібліотеки TinyMCE для простого форматування тексту у вікні "
"браузера."


code:
Twitter Bridge Plugin
=====================

This Twitter "bridge" plugin allows you to integrate your StatusNet
instance with Twitter.  Installing it will allow your users to:

    - automatically post notices to their Twitter accounts
    - automatically subscribe to other Twitter users who are also using
      your StatusNet install, if possible (requires running a daemon)
    - import their Twitter friends' tweets (requires running a daemon)
    - allow users to authenticate using Twitter ('Sign in with Twitter')

Installation
------------

OAuth 1.0a (http://oauth.net) is used to to access protected resources
on Twitter. To use Twitter bridging you will need to register your
instance of StatusNet as an application on Twitter (http://twitter.com/apps).
During the application registration process your application will be assigned
a "consumer" key and secret, which the plugin will use to make OAuth requests
to Twitter. You can either pass the consumer key and secret in when you
enable the plugin, or set it using the Twitter administration panel.

When registering your application with Twitter set the type to "Browser"
and your Callback URL to:

    http://example.org/mublog/twitter/authorization

(Change "example.org" to your site domain and "mublog" to your site
path.)

The default access type should be "Read & Write".

To enable the plugin, add the following to your config.php:

    addPlugin(
        'TwitterBridge',
        array(
            'consumer_key'    => 'YOUR_CONSUMER_KEY',
            'consumer_secret' => 'YOUR_CONSUMER_SECRET'
        )
    );

or if you want to set the variables from the website's administration panel:

   addPlugin('TwitterBridge');

After saving your configuration file, please run 'php scripts/upgrade.php'
and also restart the background daemons if they are active on your instance.

Administration panel
--------------------

To access the administration panel, you'll need to use a user with the
"administrator" role (see: scripts/userrole.php).

Sign in with Twitter
--------------------

GNU social optionally allows users to register and authenticate using their
Twitter credentials via the "Sign in with Twitter" pattern described here:

    https://dev.twitter.com/web/sign-in

The option is _on_ by default when you install the plugin, but it can
disabled via the Twitter bridge administration panel, or by adding the
following line to your config.php:

    $config['twitter']['signin'] = false;

Daemons
-------

For friend syncing and importing Twitter tweets, running two
additional daemon scripts is necessary: synctwitterfriends.php and
twitterstatusfetcher.php.

In the daemons subdirectory of the plugin are three scripts:

* Twitter Friends Syncing (daemons/synctwitterfriends.php)

Users may set a flag in their settings ("Subscribe to my Twitter friends
here" under the Twitter tab) to have StatusNet attempt to locate and
subscribe to "friends" (people they "follow") on Twitter who also have
accounts on your StatusNet system, and who have previously set up a link
for automatically posting notices to Twitter.

The plugin will start this daemon when you run scripts/startdaemons.sh.

* Importing statuses from Twitter (daemons/twitterstatusfetcher.php)

You can allow uses to enable importing of your friends' Twitter
timelines either in the Twitter bridge administration panel or in your
config.php using the following configuration line:

    $config['twitterimport']['enabled'] = true;

The plugin will then start the TwitterStatusFetcher daemon along with the
other daemons when you run scripts/startdaemons.sh.

Additionally, you will want to set the integration source variable,
which will keep notices posted to Twitter via StatusNet from looping
back.  You can do this in the Twitter bridge administration panel, or
via config.php. The integration source should be set to the name of your
application _exactly_ as you specified it on the settings page for your
StatusNet application on Twitter, e.g.:

    $config['integration']['source'] = 'YourApp';

* TwitterQueueHandler (daemons/twitterqueuehandler.php)

This script sends queued notices to Twitter for user who have opted to
set up Twitter bridging.

It's not strictly necessary to run this queue handler, and sites that
haven't enabled queuing are still able to push notices to Twitter, but
for larger sites and sites that wish to improve performance the script
allows notices to be sent "offline" via a separate process.

StatusNet will automatically use the TwitterQueueHandler if you have
enabled the queuing subsystem.  See the "Queues and daemons" section of
the main README file for more information about how to do that.

path: plugins/TwitterBridge/README


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Plugin
 * @package   StatusNet
 * @author    Zach Copley <zach@status.net>
 * @author    Julien C <chaumond@gmail.com>
 * @copyright 2009-2010 Control Yourself, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    exit(1);
}

require_once __DIR__ . '/twitter.php';

/**
 * Plugin for sending and importing Twitter statuses
 *
 * This class allows users to link their Twitter accounts
 *
 * Depends on Favorite plugin.
 *
 * @category Plugin
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @author   Julien C <chaumond@gmail.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 * @link     http://twitter.com/
 */
class TwitterBridgePlugin extends Plugin
{
    const VERSION = GNUSOCIAL_VERSION;
    public $adminImportControl = false; // Should the 'import' checkbox be exposed in the admin panel?

    /**
     * Initializer for the plugin.
     */
    function initialize()
    {
        // Allow the key and secret to be passed in
        // Control panel will override

        if (isset($this->consumer_key)) {
            $key = common_config('twitter', 'consumer_key');
            if (empty($key)) {
                Config::save('twitter', 'consumer_key', $this->consumer_key);
            }
        }

        if (isset($this->consumer_secret)) {
            $secret = common_config('twitter', 'consumer_secret');
            if (empty($secret)) {
                Config::save(
                    'twitter',
                    'consumer_secret',
                    $this->consumer_secret
                );
            }
        }
    }

    /**
     * Check to see if there is a consumer key and secret defined
     * for Twitter integration.
     *
     * @return boolean result
     */
    static function hasKeys()
    {
        $ckey    = common_config('twitter', 'consumer_key');
        $csecret = common_config('twitter', 'consumer_secret');

        if (empty($ckey) && empty($csecret)) {
            $ckey    = common_config('twitter', 'global_consumer_key');
            $csecret = common_config('twitter', 'global_consumer_secret');
        }

        if (!empty($ckey) && !empty($csecret)) {
            return true;
        }

        return false;
    }

    /**
     * Add Twitter-related paths to the router table
     *
     * Hook for RouterInitialized event.
     *
     * @param URLMapper $m path-to-action mapper
     *
     * @return boolean hook return
     */
    public function onRouterInitialized(URLMapper $m)
    {
        $m->connect('panel/twitter', array('action' => 'twitteradminpanel'));

        if (self::hasKeys()) {
            $m->connect(
                'twitter/authorization',
                array('action' => 'twitterauthorization')
            );
            $m->connect(
                'settings/twitter', array(
                    'action' => 'twittersettings'
                    )
                );
            if (common_config('twitter', 'signin')) {
                $m->connect(
                    'main/twitterlogin',
                    array('action' => 'twitterlogin')
                );
            }
        }

        return true;
    }

    /*
     * Add a login tab for 'Sign in with Twitter'
     *
     * @param Action $action the current action
     *
     * @return void
     */
    function onEndLoginGroupNav($action)
    {
        $action_name = $action->trimmed('action');

        if (self::hasKeys() && common_config('twitter', 'signin')) {
            $action->menuItem(
                common_local_url('twitterlogin'),
                // TRANS: Menu item in login navigation.
                _m('MENU','Twitter'),
                // TRANS: Title for menu item in login navigation.
                _m('Login or register using Twitter.'),
                'twitterlogin' === $action_name
            );
        }

        return true;
    }

    /**
     * Add the Twitter Settings page to the Connect Settings menu
     *
     * @param Action $action The calling page
     *
     * @return boolean hook return
     */
    function onEndConnectSettingsNav($action)
    {
        if (self::hasKeys()) {
            $action_name = $action->trimmed('action');

            $action->menuItem(
                common_local_url('twittersettings'),
                // TRANS: Menu item in connection settings navigation.
                _m('MENU','Twitter'),
                // TRANS: Title for menu item in connection settings navigation.
                _m('Twitter integration options'),
                $action_name === 'twittersettings'
            );
        }
        return true;
    }

    /**
     * Add a Twitter queue item for each notice
     *
     * @param Notice $notice      the notice
     * @param array  &$transports the list of transports (queues)
     *
     * @return boolean hook return
     */
    function onStartEnqueueNotice($notice, &$transports)
    {
        if (self::hasKeys() && $notice->isLocal() && $notice->inScope(null)) {
            // Avoid a possible loop
            if ($notice->source != 'twitter') {
                array_push($transports, 'twitter');
            }
        }
        return true;
    }

    /**
     * Add Twitter bridge daemons to the list of daemons to start
     *
     * @param array $daemons the list fo daemons to run
     *
     * @return boolean hook return
     */
    function onGetValidDaemons(&$daemons)
    {
        if (self::hasKeys()) {
            array_push(
                $daemons,
                INSTALLDIR
                . '/plugins/TwitterBridge/daemons/synctwitterfriends.php'
            );
            if (common_config('twitterimport', 'enabled')) {
                array_push(
                    $daemons,
                    INSTALLDIR
                    . '/plugins/TwitterBridge/daemons/twitterstatusfetcher.php'
                    );
            }
        }

        return true;
    }

    /**
     * Register Twitter notice queue handler
     *
     * @param QueueManager $manager
     *
     * @return boolean hook return
     */
    function onEndInitializeQueueManager($manager)
    {
        if (self::hasKeys()) {
            // Outgoing notices -> twitter
            $manager->connect('twitter', 'TwitterQueueHandler');

            // Incoming statuses <- twitter
            $manager->connect('tweetin', 'TweetInQueueHandler');
        }
        return true;
    }

    /**
     * If the plugin's installed, this should be accessible to admins
     */
    function onAdminPanelCheck($name, &$isOK)
    {
        if ($name == 'twitter') {
            $isOK = true;
            return false;
        }
        return true;
    }

    /**
     * Add a Twitter tab to the admin panel
     *
     * @param Widget $nav Admin panel nav
     *
     * @return boolean hook value
     */

    function onEndAdminPanelNav($nav)
    {
        if (AdminPanelAction::canAdmin('twitter')) {

            $action_name = $nav->action->trimmed('action');

            $nav->out->menuItem(
                common_local_url('twitteradminpanel'),
                // TRANS: Menu item in administrative panel that leads to the Twitter bridge configuration.
                _m('Twitter'),
                // TRANS: Menu item title in administrative panel that leads to the Twitter bridge configuration.
                _m('Twitter bridge configuration page.'),
                $action_name == 'twitteradminpanel',
                'nav_twitter_admin_panel'
            );
        }

        return true;
    }

    /**
     * Plugin version data
     *
     * @param array &$versions array of version blocks
     *
     * @return boolean hook value
     */
    function onPluginVersion(&$versions)
    {
        $versions[] = array(
            'name' => 'TwitterBridge',
            'version' => self::VERSION,
            'author' => 'Zach Copley, Julien C, Jean Baptiste Favre',
            'homepage' => 'http://status.net/wiki/Plugin:TwitterBridge',
            // TRANS: Plugin description.
            'rawdescription' => _m('The Twitter "bridge" plugin allows integration ' .
                'of a StatusNet instance with ' .
                'Twitter.'
            )
        );
        return true;
    }

    /**
     * Expose the adminImportControl setting to the administration panel code.
     * This allows us to disable the import bridge enabling checkbox for administrators,
     * since on a bulk farm site we can't yet automate the import daemon setup.
     *
     * @return boolean hook value;
     */
    function onTwitterBridgeAdminImportControl()
    {
        return (bool)$this->adminImportControl;
    }

    /**
     * When the site is set to ssl=sometimes mode, we should make sure our
     * various auth-related pages are on SSL to keep things looking happy.
     * Although we're not submitting passwords directly, we do link out to
     * an authentication source and it's a lot happier if we've got some
     * protection against MitM.
     *
     * @param string $action name
     * @param boolean $ssl outval to force SSL
     * @return mixed hook return value
     */
    function onSensitiveAction($action, &$ssl)
    {
        $sensitive = array('twitteradminpanel',
                           'twittersettings',
                           'twitterauthorization',
                           'twitterlogin');
        if (in_array($action, $sensitive)) {
            $ssl = true;
            return false;
        } else {
            return true;
        }
    }

    /**
     * Database schema setup
     *
     * We maintain a table mapping StatusNet notices to Twitter statuses
     *
     * @see Schema
     * @see ColumnDef
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function onCheckSchema()
    {
        $schema = Schema::get();

        // For saving the last-synched status of various timelines
        // home_timeline, messages (in), messages (out), ...
        $schema->ensureTable('twitter_synch_status', Twitter_synch_status::schemaDef());

        // For storing user-submitted flags on profiles
        $schema->ensureTable('notice_to_status', Notice_to_status::schemaDef());

        return true;
    }

    /**
     * If a notice gets deleted, remove the Notice_to_status mapping and
     * delete the status on Twitter.
     *
     * @param User   $user   The user doing the deleting
     * @param Notice $notice The notice getting deleted
     *
     * @return boolean hook value
     */
    function onStartDeleteOwnNotice(User $user, Notice $notice)
    {
        $n2s = Notice_to_status::getKV('notice_id', $notice->id);

        if (!empty($n2s)) {

            $flink = Foreign_link::getByUserID($notice->profile_id,
                                               TWITTER_SERVICE); // twitter service

            if (empty($flink)) {
                return true;
            }

            if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
                $this->log(LOG_INFO, "Skipping deleting notice for {$notice->id} since link is not OAuth.");
                return true;
            }

            try {
                $token = TwitterOAuthClient::unpackToken($flink->credentials);
                $client = new TwitterOAuthClient($token->key, $token->secret);

                $client->statusesDestroy($n2s->status_id);
            } catch (Exception $e) {
                common_log(LOG_ERR, "Error attempting to delete bridged notice from Twitter: " . $e->getMessage());
            }

            $n2s->delete();
        }
        return true;
    }

    /**
     * Notify remote users when their notices get favorited.
     *
     * @param Profile or User $profile of local user doing the faving
     * @param Notice $notice being favored
     * @return hook return value
     */
    function onEndFavorNotice(Profile $profile, Notice $notice)
    {
        $flink = Foreign_link::getByUserID($profile->id,
                                           TWITTER_SERVICE); // twitter service

        if (empty($flink)) {
            return true;
        }

        if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
            $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth.");
            return true;
        }

        $status_id = twitter_status_id($notice);

        if (empty($status_id)) {
            return true;
        }

        try {
            $token = TwitterOAuthClient::unpackToken($flink->credentials);
            $client = new TwitterOAuthClient($token->key, $token->secret);

            $client->favoritesCreate($status_id);
        } catch (Exception $e) {
            common_log(LOG_ERR, "Error attempting to favorite bridged notice on Twitter: " . $e->getMessage());
        }

        return true;
    }

    /**
     * Notify remote users when their notices get de-favorited.
     *
     * @param Profile $profile Profile person doing the de-faving
     * @param Notice  $notice  Notice being favored
     *
     * @return hook return value
     */
    function onEndDisfavorNotice(Profile $profile, Notice $notice)
    {
        $flink = Foreign_link::getByUserID($profile->id,
                                           TWITTER_SERVICE); // twitter service

        if (empty($flink)) {
            return true;
        }

        if (!TwitterOAuthClient::isPackedToken($flink->credentials)) {
            $this->log(LOG_INFO, "Skipping fave processing for {$profile->id} since link is not OAuth.");
            return true;
        }

        $status_id = twitter_status_id($notice);

        if (empty($status_id)) {
            return true;
        }

        try {
            $token = TwitterOAuthClient::unpackToken($flink->credentials);
            $client = new TwitterOAuthClient($token->key, $token->secret);

            $client->favoritesDestroy($status_id);
        } catch (Exception $e) {
            common_log(LOG_ERR, "Error attempting to unfavorite bridged notice on Twitter: " . $e->getMessage());
        }

        return true;
    }

    function onStartGetProfileUri($profile, &$uri)
    {
        if (preg_match('!^https?://twitter.com/!', $profile->profileurl)) {
            $uri = $profile->profileurl;
            return false;
        }
        return true;
    }

    /**
     * Add links in the user's profile block to their Twitter profile URL.
     *
     * @param Profile $profile The profile being shown
     * @param Array   &$links  Writeable array of arrays (href, text, image).
     *
     * @return boolean hook value (true)
     */

    function onOtherAccountProfiles($profile, &$links)
    {
        $fuser = null;

        $flink = Foreign_link::getByUserID($profile->id, TWITTER_SERVICE);

        if (!empty($flink)) {
            $fuser = $flink->getForeignUser();

            if (!empty($fuser)) {
                $links[] = array("href" => $fuser->uri,
                                 "text" => sprintf(_("@%s on Twitter"), $fuser->nickname),
                                 "image" => $this->path("icons/twitter-bird-white-on-blue.png"));
            }
        }

        return true;
    }

    public function onEndShowHeadElements(Action $action)
    {
        if (!($action instanceof AttachmentAction)) {
            return true;
        }

        /* Twitter card support. See https://dev.twitter.com/docs/cards */
        /* @fixme: should we display twitter cards only for attachments posted
         *         by local users ? Seems mandatory to display twitter:creator
         *
         * Author: jbfavre
         */
        switch ($action->attachment->mimetype) {
            case 'image/pjpeg':
            case 'image/jpeg':
            case 'image/jpg':
            case 'image/png':
            case 'image/gif':
                $action->element('meta', array('name'    => 'twitter:card',
                                             'content' => 'photo'),
                                       null);
                $action->element('meta', array('name'    => 'twitter:url',
                                             'content' => common_local_url('attachment',
                                                              array('attachment' => $action->attachment->id))),
                                       null );
                $action->element('meta', array('name'    => 'twitter:image',
                                             'content' => $action->attachment->url));
                $action->element('meta', array('name'    => 'twitter:title',
                                             'content' => $action->attachment->title));

                $ns = new AttachmentNoticeSection($action);
                $notices = $ns->getNotices();
                $noticeArray = $notices->fetchAll();

                // Should not have more than 1 notice for this attachment.
                if( count($noticeArray) != 1 ) { break; }
                $post = $noticeArray[0];

                $flink = Foreign_link::getByUserID($post->profile_id, TWITTER_SERVICE);
                if( $flink ) { // Our local user has registered Twitter Gateway
                    $fuser = Foreign_user::getForeignUser($flink->foreign_id, TWITTER_SERVICE);
                    if( $fuser ) { // Got nickname for local user's Twitter account
                        $action->element('meta', array('name'    => 'twitter:creator',
                                                     'content' => '@'.$fuser->nickname));
                    }
                }
                break;
            default: break;
        }

        return true;
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Twitter bridge administration panel
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Settings
 * @package   StatusNet
 * @author    Zach Copley <zach@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Administer global Twitter bridge settings
 *
 * @category Admin
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */
class TwitteradminpanelAction extends AdminPanelAction
{
    /**
     * Returns the page title
     *
     * @return string page title
     */
    function title()
    {
        // TRANS: Page title for Twitter administration panel.
        return _m('TITLE','Twitter');
    }

    /**
     * Instructions for using this form.
     *
     * @return string instructions
     */
    function getInstructions()
    {
        // TRANS: Instructions for Twitter bridge administration page.
        return _m('Twitter bridge settings');
    }

    /**
     * Show the Twitter admin panel form
     *
     * @return void
     */
    function showForm()
    {
        $form = new TwitterAdminPanelForm($this);
        $form->show();
        return;
    }

    /**
     * Save settings from the form
     *
     * @return void
     */
    function saveSettings()
    {
        static $settings = array(
            'twitter'     => array('consumer_key', 'consumer_secret'),
            'integration' => array('source')
        );

        static $booleans = array(
            'twitter'       => array('signin')
        );
        if (Event::handle('TwitterBridgeAdminImportControl')) {
            $booleans['twitterimport'] = array('enabled');
        }

        $values = array();

        foreach ($settings as $section => $parts) {
            foreach ($parts as $setting) {
                $values[$section][$setting]
                    = $this->trimmed($setting);
            }
        }

        foreach ($booleans as $section => $parts) {
            foreach ($parts as $setting) {
                $values[$section][$setting]
                    = ($this->boolean($setting)) ? 1 : 0;
            }
        }

        // This throws an exception on validation errors

        $this->validate($values);

        // assert(all values are valid);

        $config = new Config();

        $config->query('BEGIN');

        foreach ($settings as $section => $parts) {
            foreach ($parts as $setting) {
                Config::save($section, $setting, $values[$section][$setting]);
            }
        }

        foreach ($booleans as $section => $parts) {
            foreach ($parts as $setting) {
                Config::save($section, $setting, $values[$section][$setting]);
            }
        }

        $config->query('COMMIT');

        // Flush the router cache: we may have enabled/disabled bridging,
        // which will add or remove some actions.
        $cache = Cache::instance();
        $cache->delete(Router::cacheKey());

        return;
    }

    function validate(&$values)
    {
        // Validate consumer key and secret (can't be too long)

        if (mb_strlen($values['twitter']['consumer_key']) > 255) {
            $this->clientError(
                // TRANS: Client error displayed when a consumer key is invalid because it is too long.
                _m('Invalid consumer key. Maximum length is 255 characters.')
            );
        }

        if (mb_strlen($values['twitter']['consumer_secret']) > 255) {
            $this->clientError(
                // TRANS: Client error displayed when a consumer secret is invalid because it is too long.
                _m('Invalid consumer secret. Maximum length is 255 characters.')
            );
        }
    }

    function isImportEnabled()
    {
        // Since daemon setup isn't automated yet...
        // @todo: if merged into main queues, detect presence of daemon config
        return true;
    }
}

class TwitterAdminPanelForm extends AdminForm
{
    /**
     * ID of the form
     *
     * @return int ID of the form
     */
    function id()
    {
        return 'twitteradminpanel';
    }

    /**
     * class of the form
     *
     * @return string class of the form
     */
    function formClass()
    {
        return 'form_settings';
    }

    /**
     * Action of the form
     *
     * @return string URL of the action
     */
    function action()
    {
        return common_local_url('twitteradminpanel');
    }

    /**
     * Data elements of the form
     *
     * @return void
     */
    function formData()
    {
        $this->out->elementStart(
            'fieldset',
            array('id' => 'settings_twitter-application')
        );
        // TRANS: Fieldset legend for Twitter application settings.
        $this->out->element('legend', null, _m('Twitter application settings'));
        $this->out->elementStart('ul', 'form_data');

        $this->li();
        $this->input(
            'consumer_key',
            // TRANS: Field label for Twitter assigned consumer key.
            _m('Consumer key'),
            // TRANS: Field title for Twitter assigned consumer key.
            _m('The consumer key assigned by Twitter.'),
            'twitter'
        );
        $this->unli();

        $this->li();
        $this->input(
            'consumer_secret',
            // TRANS: Field label for Twitter assigned consumer secret.
            _m('Consumer secret'),
            // TRANS: Field title for Twitter assigned consumer secret.
            _m('The consumer secret assigned by Twitter.'),
            'twitter'
        );
        $this->unli();

        $globalConsumerKey = common_config('twitter', 'global_consumer_key');
        $globalConsumerSec = common_config('twitter', 'global_consumer_secret');

        if (!empty($globalConsumerKey) && !empty($globalConsumerSec)) {
            $this->li();
            // TRANS: Form guide displayed when two required fields have already been provided.
            $this->out->element('p', 'form_guide', _m('Note: A global consumer key and secret are set.'));
            $this->unli();
        }

        $this->li();
        $this->input(
            'source',
            // TRANS: Field label for Twitter application name.
            _m('Integration source'),
            // TRANS: Field title for Twitter application name.
            _m('The name of your Twitter application.'),
            'integration'
        );
        $this->unli();

        $this->out->elementEnd('ul');
        $this->out->elementEnd('fieldset');

        $this->out->elementStart(
            'fieldset',
            array('id' => 'settings_twitter-options')
        );
        // TRANS: Fieldset legend for Twitter integration options.
        $this->out->element('legend', null, _m('Options'));

        $this->out->elementStart('ul', 'form_data');

        $this->li();

        $this->out->checkbox(
            // TRANS: Checkbox label for global setting.
            'signin', _m('Enable "Sign-in with Twitter"'),
            (bool) $this->value('signin', 'twitter'),
            // TRANS: Checkbox title.
            _m('This allow users to login with their Twitter credentials.')
        );
        $this->unli();

        if (Event::handle('TwitterBridgeAdminImportControl')) {
            $this->li();
            $this->out->checkbox(
                // TRANS: Checkbox label for global setting.
                'enabled', _m('Enable Twitter import'),
                (bool) $this->value('enabled', 'twitterimport'),
                // TRANS: Checkbox title for global setting.
                _m('Allow users to import their Twitter friends\' timelines. Requires daemons to be manually configured.')
            );
            $this->unli();
        }

        $this->out->elementEnd('ul');

        $this->out->elementEnd('fieldset');
    }

    /**
     * Action elements
     *
     * @return void
     */
    function formActions()
    {
        // TRANS: Button text for saving the administrative Twitter bridge settings.
        $this->out->submit('submit', _m('BUTTON','Save'), 'submit', null,
        // TRANS: Button title for saving the administrative Twitter bridge settings.
        _m('Save the Twitter bridge settings.'));
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * Class for doing OAuth authentication against Twitter
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Plugin
 * @package   StatusNet
 * @author    Zach Copley <zach@status.net>
 * @author    Julien C <chaumond@gmail.com>
 * @copyright 2009-2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET') && !defined('LACONICA')) {
    exit(1);
}

require_once dirname(__DIR__) . '/twitter.php';

/**
 * Class for doing OAuth authentication against Twitter
 *
 * Peforms the OAuth "dance" between StatusNet and Twitter -- requests a token,
 * authorizes it, and exchanges it for an access token.  It also creates a link
 * (Foreign_link) between the StatusNet user and Twitter user and stores the
 * access token and secret in the link.
 *
 * @category Plugin
 * @package  StatusNet
 * @author   Zach Copley <zach@status.net>
 * @author   Julien C <chaumond@gmail.com>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 */
class TwitterauthorizationAction extends Action
{
    var $twuid        = null;
    var $tw_fields    = null;
    var $access_token = null;
    var $signin       = null;
    var $verifier     = null;

    /**
     * Initialize class members. Looks for 'oauth_token' parameter.
     *
     * @param array $args misc. arguments
     *
     * @return boolean true
     */
    function prepare($args)
    {
        parent::prepare($args);

        $this->signin      = $this->boolean('signin');
        $this->oauth_token = $this->arg('oauth_token');
        $this->verifier    = $this->arg('oauth_verifier');

        return true;
    }

    /**
     * Handler method
     *
     * @param array $args is ignored since it's now passed in in prepare()
     *
     * @return nothing
     */
    function handle($args)
    {
        parent::handle($args);

        if (common_logged_in()) {
            $user  = common_current_user();
            $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);

            // If there's already a foreign link record and a foreign user
            // it means the accounts are already linked, and this is unecessary.
            // So go back.

            if (isset($flink)) {
                $fuser = $flink->getForeignUser();
                if (!empty($fuser)) {
                    common_redirect(common_local_url('twittersettings'));
                }
            }
        }

        if ($_SERVER['REQUEST_METHOD'] == 'POST') {

            // User was not logged in to StatusNet before

            $this->twuid = $this->trimmed('twuid');

            $this->tw_fields = array('screen_name' => $this->trimmed('tw_fields_screen_name'),
                                     'fullname' => $this->trimmed('tw_fields_fullname'));

            $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret'));

            $token = $this->trimmed('token');

            if (!$token || $token != common_session_token()) {
                // TRANS: Client error displayed when the session token does not match or is not given.
                $this->showForm(_m('There was a problem with your session token. Try again, please.'));
                return;
            }

            if ($this->arg('create')) {
                if (!$this->boolean('license')) {
                    // TRANS: Form validation error displayed when the checkbox to agree to the license has not been checked.
                    $this->showForm(_m('You cannot register if you do not agree to the license.'),
                                    $this->trimmed('newname'));
                    return;
                }
                $this->createNewUser();
            } else if ($this->arg('connect')) {
                $this->connectNewUser();
            } else {
                common_debug('Twitter bridge - ' . print_r($this->args, true));
                // TRANS: Form validation error displayed when an unhandled error occurs.
                $this->showForm(_m('Something weird happened.'),
                                $this->trimmed('newname'));
            }
        } else {
            // $this->oauth_token is only populated once Twitter authorizes our
            // request token. If it's empty we're at the beginning of the auth
            // process

            if (empty($this->oauth_token)) {
                $this->authorizeRequestToken();
            } else {
                $this->saveAccessToken();
            }
        }
    }

    /**
     * Asks Twitter for a request token, and then redirects to Twitter
     * to authorize it.
     *
     * @return nothing
     */
    function authorizeRequestToken()
    {
        try {
            // Get a new request token and authorize it

            $client  = new TwitterOAuthClient();
            $req_tok = $client->getRequestToken();

            // Sock the request token away in the session temporarily

            $_SESSION['twitter_request_token']        = $req_tok->key;
            $_SESSION['twitter_request_token_secret'] = $req_tok->secret;

            $auth_link = $client->getAuthorizeLink($req_tok, $this->signin);
        } catch (OAuthClientException $e) {
            $msg = sprintf(
                'OAuth client error - code: %1s, msg: %2s',
                $e->getCode(),
                $e->getMessage()
            );
            common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
            $this->serverError(
                // TRANS: Server error displayed when linking to a Twitter account fails.
                _m('Could not link your Twitter account.')
            );
        }

        common_redirect($auth_link);
    }

    /**
     * Called when Twitter returns an authorized request token. Exchanges
     * it for an access token and stores it.
     *
     * @return nothing
     */
    function saveAccessToken()
    {
        // Check to make sure Twitter returned the same request
        // token we sent them

        if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
            $this->serverError(
                // TRANS: Server error displayed when linking to a Twitter account fails because of an incorrect oauth_token.
                _m('Could not link your Twitter account: oauth_token mismatch.')
            );
        }

        $twitter_user = null;

        try {

            $client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
                $_SESSION['twitter_request_token_secret']);

            // Exchange the request token for an access token

            $atok = $client->getAccessToken($this->verifier);

            // Test the access token and get the user's Twitter info

            $client       = new TwitterOAuthClient($atok->key, $atok->secret);
            $twitter_user = $client->verifyCredentials();

        } catch (OAuthClientException $e) {
            $msg = sprintf(
                'OAuth client error - code: %1$s, msg: %2$s',
                $e->getCode(),
                $e->getMessage()
            );
            common_log(LOG_INFO, 'Twitter bridge - ' . $msg);
            $this->serverError(
                // TRANS: Server error displayed when linking to a Twitter account fails.
                _m('Could not link your Twitter account.')
            );
        }

        if (common_logged_in()) {
            // Save the access token and Twitter user info

            $user = common_current_user();
            $this->saveForeignLink($user->id, $twitter_user->id, $atok);
            save_twitter_user($twitter_user->id, $twitter_user->screen_name);

        } else {

            $this->twuid = $twitter_user->id;
            $this->tw_fields = array("screen_name" => $twitter_user->screen_name,
                                     "fullname" => $twitter_user->name);
            $this->access_token = $atok;
            $this->tryLogin();
        }

        // Clean up the the mess we made in the session

        unset($_SESSION['twitter_request_token']);
        unset($_SESSION['twitter_request_token_secret']);

        if (common_logged_in()) {
            common_redirect(common_local_url('twittersettings'));
        }
    }

    /**
     * Saves a Foreign_link between Twitter user and local user,
     * which includes the access token and secret.
     *
     * @param int        $user_id StatusNet user ID
     * @param int        $twuid   Twitter user ID
     * @param OAuthToken $token   the access token to save
     *
     * @return nothing
     */
    function saveForeignLink($user_id, $twuid, $access_token)
    {
        $flink = new Foreign_link();

        $flink->user_id = $user_id;
        $flink->service = TWITTER_SERVICE;

        // delete stale flink, if any
        $result = $flink->find(true);

        if (!empty($result)) {
            $flink->safeDelete();
        }

        $flink->user_id     = $user_id;
        $flink->foreign_id  = $twuid;
        $flink->service     = TWITTER_SERVICE;

        $creds = TwitterOAuthClient::packToken($access_token);

        $flink->credentials = $creds;
        $flink->created     = common_sql_now();

        // Defaults: noticesync on, everything else off

        $flink->set_flags(true, false, false, false);

        $flink_id = $flink->insert();

        if (empty($flink_id)) {
            common_log_db_error($flink, 'INSERT', __FILE__);
            // TRANS: Server error displayed when linking to a Twitter account fails.
            $this->serverError(_m('Could not link your Twitter account.'));
        }

        return $flink_id;
    }

    function showPageNotice()
    {
        if ($this->error) {
            $this->element('div', array('class' => 'error'), $this->error);
        } else {
            $this->element('div', 'instructions',
                           // TRANS: Page instruction. %s is the StatusNet sitename.
                           sprintf(_m('This is the first time you have logged into %s so we must connect your Twitter account to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name')));
        }
    }

    function title()
    {
        // TRANS: Page title.
        return _m('Twitter Account Setup');
    }

    function showForm($error=null, $username=null)
    {
        $this->error = $error;
        $this->username = $username;

        $this->showPage();
    }

    function showPage()
    {
        parent::showPage();
    }

    /**
     * @fixme much of this duplicates core code, which is very fragile.
     * Should probably be replaced with an extensible mini version of
     * the core registration form.
     */
    function showContent()
    {
        if (!empty($this->message_text)) {
            $this->element('p', null, $this->message);
            return;
        }

        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_settings_twitter_connect',
                                          'class' => 'form_settings',
                                          'action' => common_local_url('twitterauthorization')));
        $this->elementStart('fieldset', array('id' => 'settings_twitter_connect_options'));
        // TRANS: Fieldset legend.
        $this->element('legend', null, _m('Connection options'));

        $this->hidden('access_token_key', $this->access_token->key);
        $this->hidden('access_token_secret', $this->access_token->secret);
        $this->hidden('twuid', $this->twuid);
        $this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
        $this->hidden('tw_fields_name', $this->tw_fields['fullname']);
        $this->hidden('token', common_session_token());

        // Don't allow new account creation if site is flagged as invite only
	if (common_config('site', 'inviteonly') == false) {
            $this->elementStart('fieldset');
            $this->element('legend', null,
                           // TRANS: Fieldset legend.
                           _m('Create new account'));
            $this->element('p', null,
                           // TRANS: Sub form introduction text.
                          _m('Create a new user with this nickname.'));
            $this->elementStart('ul', 'form_data');

            // Hook point for captcha etc
            Event::handle('StartRegistrationFormData', array($this));

            $this->elementStart('li');
            // TRANS: Field label.
            $this->input('newname', _m('New nickname'),
                         ($this->username) ? $this->username : '',
                         // TRANS: Field title for nickname field.
                         _m('1-64 lowercase letters or numbers, no punctuation or spaces.'));
            $this->elementEnd('li');
            $this->elementStart('li');
            // TRANS: Field label.
            $this->input('email', _m('LABEL','Email'), $this->getEmail(),
                         // TRANS: Field title for e-mail address field.
                         _m('Used only for updates, announcements, '.
                           'and password recovery'));
            $this->elementEnd('li');

            // Hook point for captcha etc
            Event::handle('EndRegistrationFormData', array($this));

            $this->elementEnd('ul');
            // TRANS: Button text for creating a new StatusNet account in the Twitter connect page.
            $this->submit('create', _m('BUTTON','Create'));
            $this->elementEnd('fieldset');
        }

        $this->elementStart('fieldset');
        $this->element('legend', null,
                       // TRANS: Fieldset legend.
                       _m('Connect existing account'));
        $this->element('p', null,
                       // TRANS: Sub form introduction text.
                       _m('If you already have an account, login with your username and password to connect it to your Twitter account.'));
        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        // TRANS: Field label.
        $this->input('nickname', _m('Existing nickname'));
        $this->elementEnd('li');
        $this->elementStart('li');
        // TRANS: Field label.
        $this->password('password', _m('Password'));
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->elementEnd('fieldset');

        $this->elementStart('fieldset');
        $this->element('legend', null,
                       // TRANS: Fieldset legend.
                       _m('License'));
        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        $this->element('input', array('type' => 'checkbox',
                                      'id' => 'license',
                                      'class' => 'checkbox',
                                      'name' => 'license',
                                      'value' => 'true'));
        $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
        // TRANS: Text for license agreement checkbox.
        // TRANS: %s is the license as configured for the StatusNet site.
        $message = _m('My text and files are available under %s ' .
                     'except this private data: password, ' .
                     'email address, IM address, and phone number.');
        $link = '' .
                htmlspecialchars(common_config('license', 'title')) .
                '';
        $this->raw(sprintf(htmlspecialchars($message), $link));
        $this->elementEnd('label');
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->elementEnd('fieldset');
        // TRANS: Button text for connecting an existing StatusNet account in the Twitter connect page..
        $this->submit('connect', _m('BUTTON','Connect'));
        $this->elementEnd('fieldset');
        $this->elementEnd('form');
    }

    /**
     * Get specified e-mail from the form, or the invite code.
     *
     * @return string
     */
    function getEmail()
    {
        $email = $this->trimmed('email');
        if (!empty($email)) {
            return $email;
        }

        // Terrible hack for invites...
        if (common_config('site', 'inviteonly')) {
            $code = $_SESSION['invitecode'];
            if ($code) {
                $invite = Invitation::getKV($code);

                if ($invite && $invite->address_type == 'email') {
                    return $invite->address;
                }
            }
        }
        return '';
    }

    function message($msg)
    {
        $this->message_text = $msg;
        $this->showPage();
    }

    function createNewUser()
    {
        if (!Event::handle('StartRegistrationTry', array($this))) {
            return;
        }

        if (common_config('site', 'closed')) {
            // TRANS: Client error displayed when trying to create a new user while creating new users is not allowed.
            $this->clientError(_m('Registration not allowed.'));
        }

        $invite = null;

        if (common_config('site', 'inviteonly')) {
            $code = $_SESSION['invitecode'];
            if (empty($code)) {
                // TRANS: Client error displayed when trying to create a new user while creating new users is not allowed.
                $this->clientError(_m('Registration not allowed.'));
            }

            $invite = Invitation::getKV($code);

            if (empty($invite)) {
                // TRANS: Client error displayed when trying to create a new user with an invalid invitation code.
                $this->clientError(_m('Not a valid invitation code.'));
            }
        }

        try {
            $nickname = Nickname::normalize($this->trimmed('newname'), true);
        } catch (NicknameException $e) {
            $this->showForm($e->getMessage());
            return;
        }

        $fullname = trim($this->tw_fields['fullname']);

        $args = array('nickname' => $nickname, 'fullname' => $fullname);

        if (!empty($invite)) {
            $args['code'] = $invite->code;
        }

        $email = $this->getEmail();
        if (!empty($email)) {
            $args['email'] = $email;
        }

        $user = User::register($args);

        if (empty($user)) {
            // TRANS: Server error displayed when creating a new user has failed.
            $this->serverError(_m('Error registering user.'));
        }

        $result = $this->saveForeignLink($user->id,
                                         $this->twuid,
                                         $this->access_token);

        save_twitter_user($this->twuid, $this->tw_fields['screen_name']);

        if (!$result) {
            // TRANS: Server error displayed when connecting a user to a Twitter user has failed.
            $this->serverError(_m('Error connecting user to Twitter.'));
        }

        common_set_user($user);
        common_real_login(true);

        common_debug('TwitterBridge Plugin - ' .
                     "Registered new user $user->id from Twitter user $this->twuid");

        Event::handle('EndRegistrationTry', array($this));

        common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)), 303);
    }

    function connectNewUser()
    {
        $nickname = $this->trimmed('nickname');
        $password = $this->trimmed('password');

        if (!common_check_user($nickname, $password)) {
            // TRANS: Form validation error displayed when connecting an existing user to a Twitter user fails because
            // TRANS: the provided username and/or password are incorrect.
            $this->showForm(_m('Invalid username or password.'));
            return;
        }

        $user = User::getKV('nickname', $nickname);

        if (!empty($user)) {
            common_debug('TwitterBridge Plugin - ' .
                         "Legit user to connect to Twitter: $nickname");
        }

        $result = $this->saveForeignLink($user->id,
                                         $this->twuid,
                                         $this->access_token);

        save_twitter_user($this->twuid, $this->tw_fields['screen_name']);

        if (!$result) {
            // TRANS: Server error displayed connecting a user to a Twitter user has failed.
            $this->serverError(_m('Error connecting user to Twitter.'));
        }

        common_debug('TwitterBridge Plugin - ' .
                     "Connected Twitter user $this->twuid to local user $user->id");

        common_set_user($user);
        common_real_login(true);

        $this->goHome($user->nickname);
    }

    function connectUser()
    {
        $user = common_current_user();

        $result = $this->flinkUser($user->id, $this->twuid);

        if (empty($result)) {
            // TRANS: Server error displayed connecting a user to a Twitter user has failed.
            $this->serverError(_m('Error connecting user to Twitter.'));
        }

        common_debug('TwitterBridge Plugin - ' .
                     "Connected Twitter user $this->twuid to local user $user->id");

        // Return to Twitter connection settings tab
        common_redirect(common_local_url('twittersettings'), 303);
    }

    function tryLogin()
    {
        common_debug('TwitterBridge Plugin - ' .
                     "Trying login for Twitter user $this->twuid.");

        $flink = Foreign_link::getByForeignID($this->twuid,
                                              TWITTER_SERVICE);

        if (!empty($flink)) {
            $user = $flink->getUser();

            if (!empty($user)) {

                common_debug('TwitterBridge Plugin - ' .
                             "Logged in Twitter user $flink->foreign_id as user $user->id ($user->nickname)");

                common_set_user($user);
                common_real_login(true);
                $this->goHome($user->nickname);
            }

        } else {

            common_debug('TwitterBridge Plugin - ' .
                         "No flink found for twuid: $this->twuid - new user");

            $this->showForm(null, $this->bestNewNickname());
        }
    }

    function goHome($nickname)
    {
        $url = common_get_returnto();
        if ($url) {
            // We don't have to return to it again
            common_set_returnto(null);
        } else {
            $url = common_local_url('all',
                                    array('nickname' =>
                                          $nickname));
        }

        common_redirect($url, 303);
    }

    function bestNewNickname()
    {
        try {
            return Nickname::normalize($this->tw_fields['fullname'], true);
        } catch (NicknameException $e) {
            return null;
        }
    }
}


code:
<?php
/**
 * StatusNet, the distributed open-source microblogging tool
 *
 * 'Sign in with Twitter' login page
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Login
 * @package   StatusNet
 * @author    Julien Chaumond <chaumond@gmail.com>
 * @author    Zach Copley <zach@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET') && !defined('LACONICA')) {
    exit(1);
}

require_once dirname(__DIR__) . '/twitter.php';

/**
 * Page for logging in with Twitter
 *
 * @category Login
 * @package  StatusNet
 * @author   Julien Chaumond <chaumond@gmail.com>
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 *
 * @see      SettingsAction
 */
class TwitterloginAction extends Action
{
    function handle($args)
    {
        parent::handle($args);

        if (common_is_real_login()) {
            // TRANS: Client error displayed when trying to log in using Twitter while already logged in to StatusNet.
            $this->clientError(_m('Already logged in.'));
        }

        $this->showPage();
    }

    function title()
    {
        // TRANS: Title for login using Twitter page.
        return _m('TITLE','Twitter Login');
    }

    function getInstructions()
    {
        // TRANS: Instructions for login using Twitter page.
        return _m('Login with your Twitter account');
    }

    function showPageNotice()
    {
        $instr = $this->getInstructions();
        $output = common_markup_to_html($instr);
        $this->elementStart('div', 'instructions');
        $this->raw($output);
        $this->elementEnd('div');
    }

    function showContent()
    {
        $this->elementStart('a', array('href' => common_local_url('twitterauthorization',
                                                                  null,
                                                                  array('signin' => true))));
        $this->element('img', array('src' => Plugin::staticPath('TwitterBridge', 'Sign-in-with-Twitter-lighter.png'),
                                    // TRANS: Alternative text for "sign in with Twitter" image.
                                    'alt' => _m('Sign in with Twitter')));
        $this->elementEnd('a');
    }

    function showLocalNav()
    {
        $nav = new LoginGroupNav($this);
        $nav->show();
    }
}