2012-04-13 11:05:16 -04:00
< ? php
/**
2017-12-01 22:55:48 -05:00
* Name : jappixmini
* Description : Provides a Facebook - like chat using Jappix Mini
* Version : 1.0 . 1
* Author : leberwurscht < leberwurscht @ hoegners . de >
*
*/
2012-04-17 19:13:18 -04:00
//
// Copyright 2012 "Leberwurscht" <leberwurscht@hoegners.de>
//
// This file is dual-licensed under the MIT license (see MIT.txt) and the AGPL license (see jappix/COPYING).
//
2012-04-13 11:05:16 -04:00
/*
2017-12-01 22:55:48 -05:00
Problem :
* jabber password should not be stored on server
* jabber password should not be sent between server and browser as soon as the user is logged in
* jabber password should not be reconstructible from communication between server and browser as soon as the user is logged in
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
Solution :
Only store an encrypted version of the jabber password on the server . The encryption key is only available to the browser
and not to the server ( at least as soon as the user is logged in ) . It can be stored using the jappix setDB function .
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
This encryption key could be the friendica password , but then this password would be stored in the browser in cleartext .
It is better to use a hash of the password .
The server should not be able to reconstruct the password , so we can ' t take the same hash the server stores . But we can
use hash ( " some_prefix " + password ) . This will however not work with OpenID logins , for this type of login the password must
be queried manually .
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
Problem :
How to discover the jabber addresses of the friendica contacts ?
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
Solution :
Each Friendica site with this addon provides a / jappixmini / module page . We go through our contacts and retrieve
this information every week using a cron hook .
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
Problem :
We do not want to make the jabber address public .
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
Solution :
When two friendica users connect using DFRN , the relation gets a DFRN ID and a keypair is generated .
Using this keypair , we can provide the jabber address only to contacts :
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
Alice :
2012-04-14 12:23:42 -04:00
signed_address = openssl_ * _encrypt ( alice_jabber_address )
2017-12-01 22:55:48 -05:00
send signed_address to Bob , who does
2012-04-14 12:23:42 -04:00
trusted_address = openssl_ * _decrypt ( signed_address )
save trusted_address
encrypted_address = openssl_ * _encrypt ( bob_jabber_address )
2017-12-01 22:55:48 -05:00
reply with encrypted_address to Alice , who does
2012-04-14 12:23:42 -04:00
decrypted_address = openssl_ * _decrypt ( encrypted_address )
save decrypted_address
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
Interface for this :
GET / jappixmini / ? role =% s & signed_address =% s & dfrn_id =% s
2012-04-14 12:23:42 -04:00
2017-12-01 22:55:48 -05:00
Response :
json ({ " status " : " ok " , " encrypted_address " : " %s " })
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
*/
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
use Friendica\App ;
2017-11-06 18:55:24 -05:00
use Friendica\Core\Config ;
2018-12-26 02:28:16 -05:00
use Friendica\Core\Hook ;
2018-01-22 14:03:11 -05:00
use Friendica\Core\L10n ;
2018-10-29 19:40:18 -04:00
use Friendica\Core\Logger ;
2017-11-06 18:55:24 -05:00
use Friendica\Core\PConfig ;
2018-08-11 16:40:48 -04:00
use Friendica\Core\Protocol ;
2018-07-21 09:13:02 -04:00
use Friendica\Database\DBA ;
2019-12-15 19:05:14 -05:00
use Friendica\DI ;
2017-12-01 22:55:48 -05:00
use Friendica\Model\User ;
2018-01-27 08:52:02 -05:00
use Friendica\Util\Network ;
2017-11-06 18:55:24 -05:00
2017-12-01 22:55:48 -05:00
function jappixmini_install ()
{
2018-12-26 02:28:16 -05:00
Hook :: register ( 'addon_settings' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings' );
Hook :: register ( 'addon_settings_post' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings_post' );
2012-04-13 11:05:16 -04:00
2018-12-26 02:28:16 -05:00
Hook :: register ( 'page_end' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_script' );
Hook :: register ( 'authenticate' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_login' );
2012-04-13 11:05:16 -04:00
2018-12-26 02:28:16 -05:00
Hook :: register ( 'cron' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_cron' );
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
// Jappix source download as required by AGPL
2018-12-26 02:28:16 -05:00
Hook :: register ( 'about_hook' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_download_source' );
2012-04-16 13:28:23 -04:00
2017-12-01 22:55:48 -05:00
// set standard configuration
$info_text = Config :: get ( " jappixmini " , " infotext " );
if ( ! $info_text )
2018-03-26 14:09:24 -04:00
Config :: set ( " jappixmini " , " infotext " , " To get the chat working, you need to know a BOSH host which works with your Jabber account. " .
2017-12-01 22:55:48 -05:00
" An example of a BOSH server that works for all accounts is https://bind.jappix.com/, but keep " .
" in mind that the BOSH server can read along all chat messages. If you know that your Jabber " .
" server also provides an own BOSH server, it is much better to use this one! "
);
2012-04-16 15:16:24 -04:00
2017-12-01 22:55:48 -05:00
$bosh_proxy = Config :: get ( " jappixmini " , " bosh_proxy " );
if ( $bosh_proxy === " " ) {
Config :: set ( " jappixmini " , " bosh_proxy " , " 1 " );
}
2012-04-17 18:32:04 -04:00
2017-12-01 22:55:48 -05:00
// set addon version so that safe updates are possible later
$addon_version = Config :: get ( " jappixmini " , " version " );
if ( $addon_version === " " ) {
Config :: set ( " jappixmini " , " version " , " 1 " );
}
2012-04-13 11:05:16 -04:00
}
2017-12-01 22:55:48 -05:00
function jappixmini_uninstall ()
{
2018-12-26 02:28:16 -05:00
Hook :: unregister ( 'addon_settings' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings' );
Hook :: unregister ( 'addon_settings_post' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings_post' );
2012-04-13 11:05:16 -04:00
2018-12-26 02:28:16 -05:00
Hook :: unregister ( 'page_end' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_script' );
Hook :: unregister ( 'authenticate' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_login' );
2012-04-13 11:05:16 -04:00
2018-12-26 02:28:16 -05:00
Hook :: unregister ( 'cron' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_cron' );
2012-04-13 11:05:16 -04:00
2018-12-26 02:28:16 -05:00
Hook :: unregister ( 'about_hook' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_download_source' );
2012-04-13 11:05:16 -04:00
}
2018-01-20 08:57:41 -05:00
function jappixmini_addon_admin ( App $a , & $o )
2017-12-01 22:55:48 -05:00
{
2012-04-14 12:23:42 -04:00
// display instructions and warnings on addon settings page for admin
2012-04-17 19:13:18 -04:00
if ( ! file_exists ( " addon/jappixmini.tgz " )) {
$o .= '<p><strong style="color:#fff;background-color:#f00">The source archive jappixmini.tgz does not exist. This is probably a violation of the Jappix License (AGPL).</strong></p>' ;
2012-04-15 06:20:53 -04:00
}
2012-04-16 13:28:23 -04:00
2012-04-16 15:04:45 -04:00
// warn if cron job has not yet been executed
2017-11-06 18:55:24 -05:00
$cron_run = Config :: get ( " jappixmini " , " last_cron_execution " );
2017-12-01 22:55:48 -05:00
if ( ! $cron_run ) {
$o .= " <p><strong>Warning: The cron job has not yet been executed. If this message is still there after some time (usually 10 minutes), this means that autosubscribe and autoaccept will not work.</strong></p> " ;
}
2012-04-16 15:04:45 -04:00
2012-04-16 15:16:24 -04:00
// bosh proxy
2017-11-06 18:55:24 -05:00
$bosh_proxy = intval ( Config :: get ( " jappixmini " , " bosh_proxy " ));
2012-04-16 15:16:24 -04:00
$bosh_proxy = intval ( $bosh_proxy ) ? ' checked="checked"' : '' ;
$o .= '<label for="jappixmini-proxy">Activate BOSH proxy</label>' ;
2017-12-01 22:55:48 -05:00
$o .= ' <input id="jappixmini-proxy" type="checkbox" name="jappixmini-proxy" value="1"' . $bosh_proxy . ' /><br />' ;
2012-04-16 15:16:24 -04:00
2013-06-22 14:05:03 -04:00
// bosh address
2017-11-06 18:55:24 -05:00
$bosh_address = Config :: get ( " jappixmini " , " bosh_address " );
2013-06-22 14:05:03 -04:00
$o .= '<p><label for="jappixmini-address">Adress of the default BOSH proxy. If enabled it overrides the user settings:</label><br />' ;
2017-12-01 22:55:48 -05:00
$o .= '<input id="jappixmini-address" type="text" name="jappixmini-address" value="' . $bosh_address . '" /></p>' ;
2013-06-22 14:05:03 -04:00
// default server address
2017-11-06 18:55:24 -05:00
$default_server = Config :: get ( " jappixmini " , " default_server " );
2013-06-22 14:05:03 -04:00
$o .= '<p><label for="jappixmini-server">Adress of the default jabber server:</label><br />' ;
2017-12-01 22:55:48 -05:00
$o .= '<input id="jappixmini-server" type="text" name="jappixmini-server" value="' . $default_server . '" /></p>' ;
2013-06-22 14:05:03 -04:00
// default user name to friendica nickname
2017-11-06 18:55:24 -05:00
$default_user = intval ( Config :: get ( " jappixmini " , " default_user " ));
2013-06-22 14:05:03 -04:00
$default_user = intval ( $default_user ) ? ' checked="checked"' : '' ;
$o .= '<label for="jappixmini-user">Set the default username to the nickname:</label>' ;
2017-12-01 22:55:48 -05:00
$o .= ' <input id="jappixmini-user" type="checkbox" name="jappixmini-defaultuser" value="1"' . $default_user . ' /><br />' ;
2013-06-22 14:05:03 -04:00
2012-04-16 13:28:23 -04:00
// info text field
2017-11-06 18:55:24 -05:00
$info_text = Config :: get ( " jappixmini " , " infotext " );
2012-04-16 15:16:24 -04:00
$o .= '<p><label for="jappixmini-infotext">Info text to help users with configuration (important if you want to provide your own BOSH host!):</label><br />' ;
2017-12-01 22:55:48 -05:00
$o .= '<textarea id="jappixmini-infotext" name="jappixmini-infotext" rows="5" cols="50">' . htmlentities ( $info_text ) . '</textarea></p>' ;
2012-04-16 15:16:24 -04:00
// submit button
2012-04-16 13:28:23 -04:00
$o .= '<input type="submit" name="jappixmini-admin-settings" value="OK" />' ;
2012-04-13 11:05:16 -04:00
}
2018-01-20 08:57:41 -05:00
function jappixmini_addon_admin_post ( App $a )
2017-12-01 22:55:48 -05:00
{
2012-04-16 13:28:23 -04:00
// set info text
$submit = $_REQUEST [ 'jappixmini-admin-settings' ];
if ( $submit ) {
$info_text = $_REQUEST [ 'jappixmini-infotext' ];
2012-04-16 15:16:24 -04:00
$bosh_proxy = intval ( $_REQUEST [ 'jappixmini-proxy' ]);
2013-06-22 14:05:03 -04:00
$default_user = intval ( $_REQUEST [ 'jappixmini-defaultuser' ]);
$bosh_address = $_REQUEST [ 'jappixmini-address' ];
$default_server = $_REQUEST [ 'jappixmini-server' ];
2017-11-06 18:55:24 -05:00
Config :: set ( " jappixmini " , " infotext " , $info_text );
Config :: set ( " jappixmini " , " bosh_proxy " , $bosh_proxy );
Config :: set ( " jappixmini " , " bosh_address " , $bosh_address );
Config :: set ( " jappixmini " , " default_server " , $default_server );
Config :: set ( " jappixmini " , " default_user " , $default_user );
2012-04-16 13:28:23 -04:00
}
2012-04-13 11:05:16 -04:00
}
2017-12-01 22:55:48 -05:00
function jappixmini_module ()
{
}
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
function jappixmini_init ()
{
// module page where other Friendica sites can submit Jabber addresses to and also can query Jabber addresses
// of local users
2012-04-14 12:23:42 -04:00
$dfrn_id = $_REQUEST [ " dfrn_id " ];
2017-12-01 22:55:48 -05:00
if ( ! $dfrn_id ) {
2018-12-26 00:39:53 -05:00
exit ();
2017-12-01 22:55:48 -05:00
}
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
$role = $_REQUEST [ " role " ];
2017-12-01 22:55:48 -05:00
if ( $role == " pub " ) {
2018-07-21 09:13:02 -04:00
$r = q ( " SELECT * FROM `contact` WHERE LENGTH(`pubkey`) AND `dfrn-id`='%s' LIMIT 1 " , DBA :: escape ( $dfrn_id ));
2017-12-01 22:55:48 -05:00
if ( ! count ( $r )) {
2018-12-26 00:39:53 -05:00
exit ();
2017-12-01 22:55:48 -05:00
}
2012-04-13 11:05:16 -04:00
2018-12-26 06:00:36 -05:00
$encrypt_func = 'openssl_public_encrypt' ;
$decrypt_func = 'openssl_public_decrypt' ;
2012-04-14 12:23:42 -04:00
$key = $r [ 0 ][ " pubkey " ];
2017-12-01 22:55:48 -05:00
} else if ( $role == " prv " ) {
2018-07-21 09:13:02 -04:00
$r = q ( " SELECT * FROM `contact` WHERE LENGTH(`prvkey`) AND `issued-id`='%s' LIMIT 1 " , DBA :: escape ( $dfrn_id ));
2017-12-01 22:55:48 -05:00
if ( ! count ( $r )) {
2018-12-26 00:39:53 -05:00
exit ();
2017-12-01 22:55:48 -05:00
}
2012-04-13 11:05:16 -04:00
2018-12-26 06:00:36 -05:00
$encrypt_func = 'openssl_private_encrypt' ;
$decrypt_func = 'openssl_private_decrypt' ;
2012-04-14 12:23:42 -04:00
$key = $r [ 0 ][ " prvkey " ];
} else {
2018-12-26 00:39:53 -05:00
exit ();
2012-04-13 11:05:16 -04:00
}
2012-04-14 12:23:42 -04:00
$uid = $r [ 0 ][ " uid " ];
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
// save the Jabber address we received
try {
$signed_address_hex = $_REQUEST [ " signed_address " ];
$signed_address = hex2bin ( $signed_address_hex );
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
$trusted_address = " " ;
$decrypt_func ( $signed_address , $trusted_address , $key );
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
$now = intval ( time ());
2017-11-06 18:55:24 -05:00
PConfig :: set ( $uid , " jappixmini " , " id: $dfrn_id " , " $now : $trusted_address " );
2012-04-14 12:23:42 -04:00
} catch ( Exception $e ) {
2017-12-01 22:55:48 -05:00
2012-04-14 12:23:42 -04:00
}
2012-04-13 11:05:16 -04:00
2018-01-20 08:57:41 -05:00
// do not return an address if user deactivated addon
2020-01-18 10:50:56 -05:00
$activated = DI :: pConfig () -> get ( $uid , 'jappixmini' , 'activate' );
2017-12-01 22:55:48 -05:00
if ( ! $activated ) {
2018-12-26 00:39:53 -05:00
exit ();
2017-12-01 22:55:48 -05:00
}
2012-04-15 16:13:18 -04:00
2012-04-14 12:23:42 -04:00
// return the requested Jabber address
try {
2020-01-18 10:50:56 -05:00
$username = DI :: pConfig () -> get ( $uid , 'jappixmini' , 'username' );
$server = DI :: pConfig () -> get ( $uid , 'jappixmini' , 'server' );
2012-04-14 12:23:42 -04:00
$address = " $username @ $server " ;
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
$encrypted_address = " " ;
$encrypt_func ( $address , $encrypted_address , $key );
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
$encrypted_address_hex = bin2hex ( $encrypted_address );
2012-04-13 11:05:16 -04:00
2018-01-15 08:15:33 -05:00
$answer = [
2017-12-01 22:55:48 -05:00
" status " => " ok " ,
" encrypted_address " => $encrypted_address_hex
2018-01-15 08:15:33 -05:00
];
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
$answer_json = json_encode ( $answer );
echo $answer_json ;
2018-12-26 00:39:53 -05:00
exit ();
2012-04-14 12:23:42 -04:00
} catch ( Exception $e ) {
2018-12-26 00:39:53 -05:00
exit ();
2012-04-14 12:23:42 -04:00
}
2012-04-13 11:05:16 -04:00
}
2017-12-01 22:55:48 -05:00
function jappixmini_settings ( App $a , & $s )
{
// addon settings for a user
2020-01-18 10:50:56 -05:00
$activate = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'activate' );
2017-12-01 22:55:48 -05:00
$activate = intval ( $activate ) ? ' checked="checked"' : '' ;
2020-01-18 10:50:56 -05:00
$dontinsertchat = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'dontinsertchat' );
2017-12-01 22:55:48 -05:00
$insertchat = ! ( intval ( $dontinsertchat ) ? ' checked="checked"' : '' );
$defaultbosh = Config :: get ( " jappixmini " , " bosh_address " );
if ( $defaultbosh != " " ) {
PConfig :: set ( local_user (), 'jappixmini' , 'bosh' , $defaultbosh );
}
2020-01-18 10:50:56 -05:00
$username = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'username' );
2017-12-01 22:55:48 -05:00
$username = htmlentities ( $username );
2020-01-18 10:50:56 -05:00
$server = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'server' );
2017-12-01 22:55:48 -05:00
$server = htmlentities ( $server );
2020-01-18 10:50:56 -05:00
$bosh = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'bosh' );
2017-12-01 22:55:48 -05:00
$bosh = htmlentities ( $bosh );
2020-01-18 10:50:56 -05:00
$password = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'password' );
$autosubscribe = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'autosubscribe' );
2017-12-01 22:55:48 -05:00
$autosubscribe = intval ( $autosubscribe ) ? ' checked="checked"' : '' ;
2020-01-18 10:50:56 -05:00
$autoapprove = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'autoapprove' );
2017-12-01 22:55:48 -05:00
$autoapprove = intval ( $autoapprove ) ? ' checked="checked"' : '' ;
2020-01-18 10:50:56 -05:00
$encrypt = intval ( DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'encrypt' ));
2017-12-01 22:55:48 -05:00
$encrypt_checked = $encrypt ? ' checked="checked"' : '' ;
$encrypt_disabled = $encrypt ? '' : ' disabled="disabled"' ;
if ( $server == " " ) {
$server = Config :: get ( " jappixmini " , " default_server " );
}
if (( $username == " " ) && Config :: get ( " jappixmini " , " default_user " )) {
$username = $a -> user [ " nickname " ];
}
$info_text = Config :: get ( " jappixmini " , " infotext " );
$info_text = htmlentities ( $info_text );
$info_text = str_replace ( " \n " , " <br /> " , $info_text );
// count contacts
$r = q ( " SELECT COUNT(1) as `cnt` FROM `pconfig` WHERE `uid`=%d AND `cat`='jappixmini' AND `k` LIKE 'id:%%' " , local_user ());
if ( count ( $r )) {
$contact_cnt = $r [ 0 ][ " cnt " ];
} else {
$contact_cnt = 0 ;
}
// count jabber addresses
$r = q ( " SELECT COUNT(1) as `cnt` FROM `pconfig` WHERE `uid`=%d AND `cat`='jappixmini' AND `k` LIKE 'id:%%' AND `v` LIKE '%%@%%' " , local_user ());
if ( count ( $r )) {
$address_cnt = $r [ 0 ][ " cnt " ];
} else {
$address_cnt = 0 ;
}
if ( ! $activate ) {
// load scripts if not yet activated so that password can be saved
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/jappix/php/get.php?t=js&g=mini.xml"></script>' . " \r \n " ;
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/jappix/php/get.php?t=js&f=presence.js~caps.js~name.js~roster.js"></script>' . " \r \n " ;
2017-12-01 22:55:48 -05:00
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/lib.js"></script>' . " \r \n " ;
2017-12-01 22:55:48 -05:00
}
$s .= '<span id="settings_jappixmini_inflated" class="settings-block fakelink" style="display: block;" onclick="openClose(\'settings_jappixmini_expanded\'); openClose(\'settings_jappixmini_inflated\');">' ;
2018-01-22 14:03:11 -05:00
$s .= '<h3>' . L10n :: t ( 'Jappix Mini' ) . '</h3>' ;
2017-12-01 22:55:48 -05:00
$s .= '</span>' ;
$s .= '<div id="settings_jappixmini_expanded" class="settings-block" style="display: none;">' ;
$s .= '<span class="fakelink" onclick="openClose(\'settings_jappixmini_expanded\'); openClose(\'settings_jappixmini_inflated\');">' ;
2018-01-22 14:03:11 -05:00
$s .= '<h3>' . L10n :: t ( 'Jappix Mini' ) . '</h3>' ;
2017-12-01 22:55:48 -05:00
$s .= '</span>' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-activate">' . L10n :: t ( 'Activate addon' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-activate" type="checkbox" name="jappixmini-activate" value="1"' . $activate . ' />' ;
$s .= '<br />' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for"jappixmini-dont-insertchat">' . L10n :: t ( 'Do <em>not</em> insert the Jappixmini Chat-Widget into the webinterface' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= '<input id="jappixmini-dont-insertchat" type="checkbox" name="jappixmini-dont-insertchat" value="1"' . $insertchat . ' />' ;
$s .= '<br />' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-username">' . L10n :: t ( 'Jabber username' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-username" type="text" name="jappixmini-username" value="' . $username . '" />' ;
$s .= '<br />' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-server">' . L10n :: t ( 'Jabber server' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-server" type="text" name="jappixmini-server" value="' . $server . '" />' ;
$s .= '<br />' ;
if ( $defaultbosh == " " ) {
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-bosh">' . L10n :: t ( 'Jabber BOSH host' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-bosh" type="text" name="jappixmini-bosh" value="' . $bosh . '" />' ;
$s .= '<br />' ;
}
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-password">' . L10n :: t ( 'Jabber password' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input type="hidden" id="jappixmini-password" name="jappixmini-encrypted-password" value="' . $password . '" />' ;
$s .= ' <input id="jappixmini-clear-password" type="password" value="" onchange="jappixmini_set_password();" />' ;
$s .= '<br />' ;
$onchange = " document.getElementById('jappixmini-friendica-password').disabled = !this.checked;jappixmini_set_password(); " ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-encrypt">' . L10n :: t ( 'Encrypt Jabber password with Friendica password (recommended)' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-encrypt" type="checkbox" name="jappixmini-encrypt" onchange="' . $onchange . '" value="1"' . $encrypt_checked . ' />' ;
$s .= '<br />' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-friendica-password">' . L10n :: t ( 'Friendica password' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-friendica-password" name="jappixmini-friendica-password" type="password" onchange="jappixmini_set_password();" value=""' . $encrypt_disabled . ' />' ;
2013-06-22 14:05:03 -04:00
$s .= '<br />' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-autoapprove">' . L10n :: t ( 'Approve subscription requests from Friendica contacts automatically' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-autoapprove" type="checkbox" name="jappixmini-autoapprove" value="1"' . $autoapprove . ' />' ;
$s .= '<br />' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-autosubscribe">' . L10n :: t ( 'Subscribe to Friendica contacts automatically' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-autosubscribe" type="checkbox" name="jappixmini-autosubscribe" value="1"' . $autosubscribe . ' />' ;
$s .= '<br />' ;
2018-01-22 14:03:11 -05:00
$s .= '<label for="jappixmini-purge">' . L10n :: t ( 'Purge internal list of jabber addresses of contacts' ) . '</label>' ;
2017-12-01 22:55:48 -05:00
$s .= ' <input id="jappixmini-purge" type="checkbox" name="jappixmini-purge" value="1" />' ;
$s .= '<br />' ;
if ( $info_text ) {
$s .= '<br />Configuration help:<p style="margin-left:2em;">' . $info_text . '</p>' ;
}
$s .= '<br />Status:<p style="margin-left:2em;">Addon knows ' . $address_cnt . ' Jabber addresses of ' . $contact_cnt . ' Friendica contacts (takes some time, usually 10 minutes, to update).</p>' ;
2018-01-22 14:03:11 -05:00
$s .= '<input type="submit" name="jappixmini-submit" value="' . L10n :: t ( 'Save Settings' ) . '" />' ;
$s .= ' <input type="button" value="' . L10n :: t ( 'Add contact' ) . '" onclick="jappixmini_addon_subscribe();" />' ;
2017-12-01 22:55:48 -05:00
$s .= '</div>' ;
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= " <script type= \" text/javascript \" >
2012-04-15 06:20:53 -04:00
function jappixmini_set_password () {
encrypt = document . getElementById ( 'jappixmini-encrypt' ) . checked ;
password = document . getElementById ( 'jappixmini-password' );
clear_password = document . getElementById ( 'jappixmini-clear-password' );
if ( encrypt ) {
friendica_password = document . getElementById ( 'jappixmini-friendica-password' );
2012-04-15 08:31:46 -04:00
if ( friendica_password ) {
jappixmini_addon_set_client_secret ( friendica_password . value );
2012-04-15 16:57:25 -04:00
jappixmini_addon_encrypt_password ( clear_password . value , function ( encrypted_password ){
password . value = encrypted_password ;
});
2012-04-15 08:31:46 -04:00
}
2012-04-15 06:20:53 -04:00
}
else {
password . value = clear_password . value ;
}
}
2012-04-13 11:05:16 -04:00
jQuery ( document ) . ready ( function () {
2012-04-15 06:20:53 -04:00
encrypt = document . getElementById ( 'jappixmini-encrypt' ) . checked ;
password = document . getElementById ( 'jappixmini-password' );
clear_password = document . getElementById ( 'jappixmini-clear-password' );
if ( encrypt ) {
2012-04-15 16:57:25 -04:00
jappixmini_addon_decrypt_password ( password . value , function ( decrypted_password ){
clear_password . value = decrypted_password ;
});
2012-04-15 06:20:53 -04:00
}
else {
clear_password . value = password . value ;
}
2012-04-13 11:05:16 -04:00
});
</ script > " ;
}
2017-12-01 22:55:48 -05:00
function jappixmini_settings_post ( App $a , & $b )
{
2012-04-14 12:23:42 -04:00
// save addon settings for a user
2017-12-01 22:55:48 -05:00
if ( ! local_user ()) {
return ;
}
2012-04-14 12:23:42 -04:00
$uid = local_user ();
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
if ( $_POST [ 'jappixmini-submit' ]) {
2012-04-15 06:20:53 -04:00
$encrypt = intval ( $b [ 'jappixmini-encrypt' ]);
if ( $encrypt ) {
// check that Jabber password was encrypted with correct Friendica password
$friendica_password = trim ( $b [ 'jappixmini-friendica-password' ]);
2017-12-01 23:03:49 -05:00
if ( ! User :: authenticate (( int ) $uid , $friendica_password )) {
2012-04-15 06:20:53 -04:00
info ( " Wrong friendica password! " );
return ;
}
}
2012-04-14 12:23:42 -04:00
$purge = intval ( $b [ 'jappixmini-purge' ]);
$username = trim ( $b [ 'jappixmini-username' ]);
2020-01-18 10:50:56 -05:00
$old_username = DI :: pConfig () -> get ( $uid , 'jappixmini' , 'username' );
2017-12-01 22:55:48 -05:00
if ( $username != $old_username ) {
$purge = 1 ;
}
2012-04-14 12:23:42 -04:00
$server = trim ( $b [ 'jappixmini-server' ]);
2020-01-18 10:50:56 -05:00
$old_server = DI :: pConfig () -> get ( $uid , 'jappixmini' , 'server' );
2017-12-01 22:55:48 -05:00
if ( $server != $old_server ) {
$purge = 1 ;
}
PConfig :: set ( $uid , 'jappixmini' , 'username' , $username );
PConfig :: set ( $uid , 'jappixmini' , 'server' , $server );
PConfig :: set ( $uid , 'jappixmini' , 'bosh' , trim ( $b [ 'jappixmini-bosh' ]));
PConfig :: set ( $uid , 'jappixmini' , 'password' , trim ( $b [ 'jappixmini-encrypted-password' ]));
PConfig :: set ( $uid , 'jappixmini' , 'autosubscribe' , intval ( $b [ 'jappixmini-autosubscribe' ]));
PConfig :: set ( $uid , 'jappixmini' , 'autoapprove' , intval ( $b [ 'jappixmini-autoapprove' ]));
PConfig :: set ( $uid , 'jappixmini' , 'activate' , intval ( $b [ 'jappixmini-activate' ]));
PConfig :: set ( $uid , 'jappixmini' , 'dontinsertchat' , intval ( $b [ 'jappixmini-dont-insertchat' ]));
PConfig :: set ( $uid , 'jappixmini' , 'encrypt' , $encrypt );
info ( 'Jappix Mini settings saved.' );
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
if ( $purge ) {
2012-04-17 16:17:06 -04:00
q ( " DELETE FROM `pconfig` WHERE `uid`= $uid AND `cat`='jappixmini' AND `k` LIKE 'id:%%' " );
2017-12-01 22:55:48 -05:00
info ( 'List of addresses purged.' );
2012-04-13 11:05:16 -04:00
}
}
}
2017-12-01 22:55:48 -05:00
function jappixmini_script ( App $a )
{
// adds the script to the page header which starts Jappix Mini
if ( ! local_user ()) {
return ;
}
2019-10-13 12:07:27 -04:00
if (( $_GET [ 'mode' ] ? ? '' ) == 'minimal' ) {
2017-12-01 22:55:48 -05:00
return ;
}
2020-01-18 10:50:56 -05:00
$activate = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'activate' );
$dontinsertchat = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'dontinsertchat' );
2017-12-01 22:55:48 -05:00
if ( ! $activate || $dontinsertchat ) {
return ;
}
2012-04-14 12:23:42 -04:00
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/jappix/php/get.php?t=js&g=mini.xml"></script>' . " \r \n " ;
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/jappix/php/get.php?t=js&f=presence.js~caps.js~name.js~roster.js"></script>' . " \r \n " ;
2017-12-01 22:55:48 -05:00
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/lib.js"></script>' . " \r \n " ;
2017-12-01 22:55:48 -05:00
2020-01-18 10:50:56 -05:00
$username = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'username' );
2017-12-01 22:55:48 -05:00
$username = str_replace ( " ' " , " \\ ' " , $username );
2020-01-18 10:50:56 -05:00
$server = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'server' );
2017-12-01 22:55:48 -05:00
$server = str_replace ( " ' " , " \\ ' " , $server );
2020-01-18 10:50:56 -05:00
$bosh = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'bosh' );
2017-12-01 22:55:48 -05:00
$bosh = str_replace ( " ' " , " \\ ' " , $bosh );
2020-01-18 10:50:56 -05:00
$encrypt = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'encrypt' );
2017-12-01 22:55:48 -05:00
$encrypt = intval ( $encrypt );
2020-01-18 10:50:56 -05:00
$password = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'password' );
2017-12-01 22:55:48 -05:00
$password = str_replace ( " ' " , " \\ ' " , $password );
2020-01-18 10:50:56 -05:00
$autoapprove = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'autoapprove' );
2017-12-01 22:55:48 -05:00
$autoapprove = intval ( $autoapprove );
2020-01-18 10:50:56 -05:00
$autosubscribe = DI :: pConfig () -> get ( local_user (), 'jappixmini' , 'autosubscribe' );
2017-12-01 22:55:48 -05:00
$autosubscribe = intval ( $autosubscribe );
// set proxy if necessary
$use_proxy = Config :: get ( 'jappixmini' , 'bosh_proxy' );
if ( $use_proxy ) {
2019-12-15 19:05:14 -05:00
$proxy = DI :: baseUrl () -> get () . '/addon/jappixmini/proxy.php' ;
2017-12-01 22:55:48 -05:00
} else {
$proxy = " " ;
}
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
// get a list of jabber accounts of the contacts
2018-01-15 08:15:33 -05:00
$contacts = [];
2017-12-01 22:55:48 -05:00
$uid = local_user ();
$rows = q ( " SELECT * FROM `pconfig` WHERE `uid`= $uid AND `cat`='jappixmini' AND `k` LIKE 'id:%%' " );
foreach ( $rows as $row ) {
$key = $row [ 'k' ];
$pos = strpos ( $key , " : " );
$dfrn_id = substr ( $key , $pos + 1 );
2018-07-21 09:13:02 -04:00
$r = q ( " SELECT `name` FROM `contact` WHERE `uid`= $uid AND (`dfrn-id`='%s' OR `issued-id`='%s') " , DBA :: escape ( $dfrn_id ), DBA :: escape ( $dfrn_id ));
2017-12-01 22:55:48 -05:00
if ( count ( $r ))
$name = $r [ 0 ][ " name " ];
$value = $row [ 'v' ];
$pos = strpos ( $value , " : " );
$address = substr ( $value , $pos + 1 );
if ( ! $address ) {
continue ;
}
if ( ! $name ) {
$name = $address ;
}
$contacts [ $address ] = $name ;
}
$contacts_json = json_encode ( $contacts );
$contacts_hash = sha1 ( $contacts_json );
// get nickname
$r = q ( " SELECT `username` FROM `user` WHERE `uid`= $uid " );
$nickname = json_encode ( $r [ 0 ][ " username " ]);
$groupchats = Config :: get ( 'jappixmini' , 'groupchats' );
//if $groupchats has no value jappix_addon_start will produce a syntax error
if ( empty ( $groupchats )) {
$groupchats = " { } " ;
}
2015-09-26 05:00:16 -04:00
2017-12-01 22:55:48 -05:00
// add javascript to start Jappix Mini
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= " <script type= \" text/javascript \" >
2012-04-13 11:05:16 -04:00
jQuery ( document ) . ready ( function () {
2013-09-27 17:33:35 -04:00
jappixmini_addon_start ( '$server' , '$username' , '$proxy' , '$bosh' , $encrypt , '$password' , $nickname , $contacts_json , '$contacts_hash' , $autoapprove , $autosubscribe , $groupchats );
2012-04-13 11:05:16 -04:00
});
</ script > " ;
2017-12-01 22:55:48 -05:00
return ;
2012-04-13 11:05:16 -04:00
}
2017-12-01 22:55:48 -05:00
function jappixmini_login ( App $a , & $o )
{
// create client secret on login to be able to encrypt jabber passwords
// for setDB and str_sha1, needed by jappixmini_addon_set_client_secret
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/jappix/php/get.php?t=js&f=datastore.js~jsjac.js"></script>' . " \r \n " ;
2012-04-15 06:20:53 -04:00
2017-12-01 22:55:48 -05:00
// for jappixmini_addon_set_client_secret
2019-12-30 14:02:08 -05:00
DI :: page ()[ 'htmlhead' ] .= '<script type="text/javascript" src="' . DI :: baseUrl () -> get () . '/addon/jappixmini/lib.js"></script>' . " \r \n " ;
2012-04-14 12:23:42 -04:00
2017-12-01 22:55:48 -05:00
// save hash of password
$o = str_replace ( " <form " , " <form onsubmit= \" jappixmini_addon_set_client_secret(this.elements['id_password'].value);return true; \" " , $o );
2012-04-13 11:05:16 -04:00
}
2017-12-01 22:55:48 -05:00
function jappixmini_cron ( App $a , $d )
{
2012-04-13 11:05:16 -04:00
// For autosubscribe/autoapprove, we need to maintain a list of jabber addresses of our contacts.
2017-11-06 18:55:24 -05:00
Config :: set ( " jappixmini " , " last_cron_execution " , $d );
2012-04-16 15:04:45 -04:00
2012-04-13 11:05:16 -04:00
// go through list of users with jabber enabled
$users = q ( " SELECT `uid` FROM `pconfig` WHERE `cat`='jappixmini' AND (`k`='autosubscribe' OR `k`='autoapprove') AND `v`='1' " );
2018-10-29 19:40:18 -04:00
Logger :: log ( " jappixmini: Update list of contacts' jabber accounts for " . count ( $users ) . " users. " );
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
if ( ! count ( $users )) {
2012-06-29 00:29:10 -04:00
return ;
2017-12-01 22:55:48 -05:00
}
2012-06-29 00:29:10 -04:00
2012-04-13 11:05:16 -04:00
foreach ( $users as $row ) {
$uid = $row [ " uid " ];
// for each user, go through list of contacts
2017-01-15 07:23:40 -05:00
$contacts = q ( " SELECT * FROM `contact` WHERE `uid`=%d AND ((LENGTH(`dfrn-id`) AND LENGTH(`pubkey`)) OR (LENGTH(`issued-id`) AND LENGTH(`prvkey`))) AND `network` = '%s' " ,
2018-08-11 16:40:48 -04:00
intval ( $uid ), DBA :: escape ( Protocol :: DFRN ));
2012-04-13 11:05:16 -04:00
foreach ( $contacts as $contact_row ) {
$request = $contact_row [ " request " ];
2017-12-01 22:55:48 -05:00
if ( ! $request ) {
continue ;
}
2012-04-13 11:05:16 -04:00
$dfrn_id = $contact_row [ " dfrn-id " ];
2012-04-14 12:23:42 -04:00
if ( $dfrn_id ) {
$key = $contact_row [ " pubkey " ];
2018-12-26 06:00:36 -05:00
$encrypt_func = 'openssl_public_encrypt' ;
$decrypt_func = 'openssl_public_decrypt' ;
2012-04-14 12:23:42 -04:00
$role = " prv " ;
} else {
2012-04-13 11:05:16 -04:00
$dfrn_id = $contact_row [ " issued-id " ];
2012-04-14 12:23:42 -04:00
$key = $contact_row [ " prvkey " ];
2018-12-26 06:00:36 -05:00
$encrypt_func = 'openssl_private_encrypt' ;
$decrypt_func = 'openssl_private_decrypt' ;
2012-04-14 12:23:42 -04:00
$role = " pub " ;
2012-04-13 11:05:16 -04:00
}
// check if jabber address already present
2020-01-18 10:50:56 -05:00
$present = DI :: pConfig () -> get ( $uid , " jappixmini " , " id: " . $dfrn_id );
2012-04-13 11:05:16 -04:00
$now = intval ( time ());
if ( $present ) {
// $present has format "timestamp:jabber_address"
$p = strpos ( $present , " : " );
$timestamp = intval ( substr ( $present , 0 , $p ));
// do not re-retrieve jabber address if last retrieval
// is not older than a week
2017-12-01 22:55:48 -05:00
if ( $now - $timestamp < 3600 * 24 * 7 ) {
continue ;
}
2012-04-13 11:05:16 -04:00
}
// construct base retrieval address
$pos = strpos ( $request , " /dfrn_request/ " );
2017-12-01 22:55:48 -05:00
if ( $pos === false ) {
continue ;
}
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
$base = substr ( $request , 0 , $pos ) . " /jappixmini?role= $role " ;
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
// construct own address
2020-01-18 10:50:56 -05:00
$username = DI :: pConfig () -> get ( $uid , 'jappixmini' , 'username' );
2017-12-01 22:55:48 -05:00
if ( ! $username ) {
continue ;
}
2020-01-18 10:50:56 -05:00
$server = DI :: pConfig () -> get ( $uid , 'jappixmini' , 'server' );
2017-12-01 22:55:48 -05:00
if ( ! $server ) {
continue ;
}
2012-04-13 11:05:16 -04:00
2017-12-01 22:55:48 -05:00
$address = $username . " @ " . $server ;
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
// sign address
$signed_address = " " ;
$encrypt_func ( $address , $signed_address , $key );
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
// construct request url
$signed_address_hex = bin2hex ( $signed_address );
2017-12-01 22:55:48 -05:00
$url = $base . " &signed_address= $signed_address_hex &dfrn_id= " . urlencode ( $dfrn_id );
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
try {
// send request
2018-01-27 11:23:04 -05:00
$answer_json = Network :: fetchUrl ( $url );
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
// parse answer
2012-04-13 11:05:16 -04:00
$answer = json_decode ( $answer_json );
2018-12-26 06:04:29 -05:00
if ( empty ( $answer -> status ) || ( $answer -> status != " ok " )) {
2017-12-01 22:55:48 -05:00
throw new Exception ();
}
2012-04-14 12:23:42 -04:00
$encrypted_address_hex = $answer -> encrypted_address ;
2017-12-01 22:55:48 -05:00
if ( ! $encrypted_address_hex ) {
throw new Exception ();
}
2012-04-14 12:23:42 -04:00
$encrypted_address = hex2bin ( $encrypted_address_hex );
2017-12-01 22:55:48 -05:00
if ( ! $encrypted_address ) {
throw new Exception ();
}
2012-04-13 11:05:16 -04:00
2012-04-14 12:23:42 -04:00
// decrypt address
$decrypted_address = " " ;
$decrypt_func ( $encrypted_address , $decrypted_address , $key );
2017-12-01 22:55:48 -05:00
if ( ! $decrypted_address ) {
throw new Exception ();
}
2012-04-14 12:23:42 -04:00
} catch ( Exception $e ) {
$decrypted_address = " " ;
2012-04-13 11:05:16 -04:00
}
// save address
2017-11-06 18:55:24 -05:00
PConfig :: set ( $uid , " jappixmini " , " id: $dfrn_id " , " $now : $decrypted_address " );
2012-04-13 11:05:16 -04:00
}
}
}
2017-12-01 22:55:48 -05:00
function jappixmini_download_source ( App $a , & $b )
{
2012-04-14 12:23:42 -04:00
// Jappix Mini source download link on About page
2012-04-13 11:05:16 -04:00
$b .= '<h1>Jappix Mini</h1>' ;
2019-12-15 19:05:14 -05:00
$b .= '<p>This site uses the jappixmini addon, which includes Jappix Mini by the <a href="' . DI :: baseUrl () -> get () . '/addon/jappixmini/jappix/AUTHORS">Jappix authors</a> and is distributed under the terms of the <a href="' . DI :: baseUrl () -> get () . '/addon/jappixmini/jappix/COPYING">GNU Affero General Public License</a>.</p>' ;
$b .= '<p>You can download the <a href="' . DI :: baseUrl () -> get () . '/addon/jappixmini.tgz">source code of the addon</a>. The rest of Friendica is distributed under compatible licenses and can be retrieved from <a href="https://github.com/friendica/friendica">https://github.com/friendica/friendica</a> and <a href="https://github.com/friendica/friendica-addons">https://github.com/friendica/friendica-addons</a></p>' ;
2012-04-13 11:05:16 -04:00
}