2012-04-13 15:05:16 +00:00
< ? php
/**
* Name : jappixmini
* Description : Inserts a jabber chat
* Version : 1.0
* Author : leberwurscht
*
*/
/*
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
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 .
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
2012-04-14 16:23:42 +00:00
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 15:05:16 +00:00
Problem :
How to discover the jabber addresses of the friendica contacts ?
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 .
Problem :
We do not want to make the jabber address public .
Solution :
When two friendica users connect using DFRN , the relation gets a DFRN ID and a keypair is generated .
2012-04-14 16:23:42 +00:00
Using this keypair , we can provide the jabber address only to contacts :
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
Alice :
signed_address = openssl_ * _encrypt ( alice_jabber_address )
send signed_address to Bob , who does
trusted_address = openssl_ * _decrypt ( signed_address )
save trusted_address
encrypted_address = openssl_ * _encrypt ( bob_jabber_address )
reply with encrypted_address to Alice , who does
decrypted_address = openssl_ * _decrypt ( encrypted_address )
save decrypted_address
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
Interface for this :
GET / jappixmini / ? role =% s & signed_address =% s & dfrn_id =% s
Response :
json ({ " status " : " ok " , " encrypted_address " : " %s " })
2012-04-13 15:05:16 +00:00
*/
function jappixmini_install () {
register_hook ( 'plugin_settings' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings' );
register_hook ( 'plugin_settings_post' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings_post' );
register_hook ( 'page_end' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_script' );
register_hook ( 'authenticate' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_login' );
register_hook ( 'cron' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_cron' );
// Jappix source download as required by AGPL
register_hook ( 'about_hook' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_download_source' );
}
function jappixmini_uninstall () {
unregister_hook ( 'plugin_settings' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings' );
unregister_hook ( 'plugin_settings_post' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_settings_post' );
unregister_hook ( 'page_end' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_script' );
unregister_hook ( 'authenticate' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_login' );
unregister_hook ( 'cron' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_cron' );
unregister_hook ( 'about_hook' , 'addon/jappixmini/jappixmini.php' , 'jappixmini_download_source' );
}
function jappixmini_plugin_admin ( & $a , & $o ) {
2012-04-14 16:23:42 +00:00
// display instructions and warnings on addon settings page for admin
2012-04-13 15:05:16 +00:00
if ( ! file_exists ( " addon/jappixmini/jappix " )) {
$o .= '<p><strong>You need to install the Jappix application, adapted for Friendica (see README).</strong></p>' ;
}
else if ( ! file_exists ( " addon/jappixmini/jappix.zip " )) {
$o .= '<p><strong style="color:#fff;background-color:#f00">The source archive jappix.zip does not exist. This is probably a violation of the Jappix License (see README).</strong></p>' ;
}
}
function jappixmini_plugin_admin_post ( & $a ) {
}
function jappixmini_module () {}
function jappixmini_init ( & $a ) {
2012-04-14 16:23:42 +00:00
// module page where other Friendica sites can submit Jabber addresses to and also can query Jabber addresses
// of local users
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$dfrn_id = $_REQUEST [ " dfrn_id " ];
if ( ! $dfrn_id ) killme ();
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$role = $_REQUEST [ " role " ];
if ( $role == " pub " ) {
$r = q ( " SELECT * FROM `contact` WHERE LENGTH(`pubkey`) AND `dfrn-id`='%s' LIMIT 1 " ,
dbesc ( $dfrn_id )
);
if ( ! count ( $r )) killme ();
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$encrypt_func = openssl_public_encrypt ;
$decrypt_func = openssl_public_decrypt ;
$key = $r [ 0 ][ " pubkey " ];
} else if ( $role == " prv " ) {
$r = q ( " SELECT * FROM `contact` WHERE LENGTH(`prvkey`) AND `issued-id`='%s' LIMIT 1 " ,
dbesc ( $dfrn_id )
);
if ( ! count ( $r )) killme ();
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$encrypt_func = openssl_private_encrypt ;
$decrypt_func = openssl_private_decrypt ;
$key = $r [ 0 ][ " prvkey " ];
} else {
2012-04-13 15:05:16 +00:00
killme ();
}
2012-04-14 16:23:42 +00:00
$uid = $r [ 0 ][ " uid " ];
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
// save the Jabber address we received
try {
$signed_address_hex = $_REQUEST [ " signed_address " ];
$signed_address = hex2bin ( $signed_address_hex );
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$trusted_address = " " ;
$decrypt_func ( $signed_address , $trusted_address , $key );
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$now = intval ( time ());
set_pconfig ( $uid , " jappixmini " , " id: $dfrn_id " , " $now : $trusted_address " );
} catch ( Exception $e ) {
}
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
// return the requested Jabber address
try {
$username = get_pconfig ( $uid , 'jappixmini' , 'username' );
$server = get_pconfig ( $uid , 'jappixmini' , 'server' );
$address = " $username @ $server " ;
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$encrypted_address = " " ;
$encrypt_func ( $address , $encrypted_address , $key );
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$encrypted_address_hex = bin2hex ( $encrypted_address );
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$answer = Array (
" status " => " ok " ,
" encrypted_address " => $encrypted_address_hex
);
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$answer_json = json_encode ( $answer );
echo $answer_json ;
killme ();
} catch ( Exception $e ) {
killme ();
}
2012-04-13 15:05:16 +00:00
}
function jappixmini_settings ( & $a , & $s ) {
2012-04-14 16:23:42 +00:00
// addon settings for a user
$activate = get_pconfig ( local_user (), 'jappixmini' , 'activate' );
$activate = intval ( $activate ) ? ' checked="checked"' : '' ;
2012-04-13 15:05:16 +00:00
$username = get_pconfig ( local_user (), 'jappixmini' , 'username' );
$username = htmlentities ( $username );
$server = get_pconfig ( local_user (), 'jappixmini' , 'server' );
$server = htmlentities ( $server );
$bosh = get_pconfig ( local_user (), 'jappixmini' , 'bosh' );
$bosh = htmlentities ( $bosh );
$encrypted_password = get_pconfig ( local_user (), 'jappixmini' , 'encrypted-password' );
$autosubscribe = get_pconfig ( local_user (), 'jappixmini' , 'autosubscribe' );
$autosubscribe = intval ( $autosubscribe ) ? ' checked="checked"' : '' ;
$autoapprove = get_pconfig ( local_user (), 'jappixmini' , 'autoapprove' );
$autoapprove = intval ( $autoapprove ) ? ' checked="checked"' : '' ;
$s .= '<div class="settings-block">' ;
$s .= '<h3>Jappix Mini addon settings</h3>' ;
$s .= '<div>' ;
$s .= '<label for="jappixmini-activate">Activate addon</label>' ;
$s .= ' <input id="jappixmini-activate" type="checkbox" name="jappixmini-activate" value="1"' . $activate . ' />' ;
$s .= '<br />' ;
$s .= '<label for="jappixmini-username">Jabber username</label>' ;
$s .= ' <input id="jappixmini-username" type="text" name="jappixmini-username" value="' . $username . '" />' ;
$s .= '<br />' ;
$s .= '<label for="jappixmini-server">Jabber server</label>' ;
$s .= ' <input id="jappixmini-server" type="text" name="jappixmini-server" value="' . $server . '" />' ;
$s .= '<br />' ;
$s .= '<label for="jappixmini-bosh">Jabber BOSH host</label>' ;
$s .= ' <input id="jappixmini-bosh" type="text" name="jappixmini-bosh" value="' . $bosh . '" />' ;
$s .= '<br />' ;
$s .= '<label for="jappixmini-password">Jabber password</label>' ;
$s .= ' <input type="hidden" id="jappixmini-encrypted-password" name="jappixmini-encrypted-password" value="' . $encrypted_password . '" />' ;
$onchange = " document.getElementById('jappixmini-encrypted-password').value = jappixmini_addon_encrypt_password(document.getElementById('jappixmini-password').value); " ;
$s .= ' <input id="jappixmini-password" type="password" value="" onchange="' . $onchange . '" />' ;
$s .= '<br />' ;
$s .= '<label for="jappixmini-autoapprove">Approve subscription requests from Friendica contacts automatically</label>' ;
$s .= ' <input id="jappixmini-autoapprove" type="checkbox" name="jappixmini-autoapprove" value="1"' . $autoapprove . ' />' ;
$s .= '<br />' ;
$s .= '<label for="jappixmini-autosubscribe">Subscribe to Friendica contacts automatically</label>' ;
$s .= ' <input id="jappixmini-autosubscribe" type="checkbox" name="jappixmini-autosubscribe" value="1"' . $autosubscribe . ' />' ;
$s .= '<br />' ;
2012-04-14 16:23:42 +00:00
$s .= '<label for="jappixmini-purge">Purge internal list of jabber addresses of contacts</label>' ;
2012-04-13 15:05:16 +00:00
$s .= ' <input id="jappixmini-purge" type="checkbox" name="jappixmini-purge" value="1" />' ;
$s .= '<br />' ;
$s .= '<input type="submit" name="jappixmini-submit" value="' . t ( 'Submit' ) . '" />' ;
$s .= '</div>' ;
$a -> page [ 'htmlhead' ] .= " <script type= \" text/javascript \" >
jQuery ( document ) . ready ( function () {
document . getElementById ( 'jappixmini-password' ) . value = jappixmini_addon_decrypt_password ( '$encrypted_password' );
});
</ script > " ;
}
function jappixmini_settings_post ( & $a , & $b ) {
2012-04-14 16:23:42 +00:00
// save addon settings for a user
2012-04-13 15:05:16 +00:00
if ( ! local_user ()) return ;
2012-04-14 16:23:42 +00:00
$uid = local_user ();
2012-04-13 15:05:16 +00:00
if ( $_POST [ 'jappixmini-submit' ]) {
2012-04-14 16:23:42 +00:00
$purge = intval ( $b [ 'jappixmini-purge' ]);
$username = trim ( $b [ 'jappixmini-username' ]);
$old_username = get_pconfig ( $uid , 'jappixmini' , 'username' );
if ( $username != $old_username ) $purge = 1 ;
$server = trim ( $b [ 'jappixmini-server' ]);
$old_server = get_pconfig ( $uid , 'jappixmini' , 'server' );
if ( $server != $old_server ) $purge = 1 ;
set_pconfig ( $uid , 'jappixmini' , 'username' , $username );
set_pconfig ( $uid , 'jappixmini' , 'server' , $server );
set_pconfig ( $uid , 'jappixmini' , 'bosh' , trim ( $b [ 'jappixmini-bosh' ]));
set_pconfig ( $uid , 'jappixmini' , 'encrypted-password' , trim ( $b [ 'jappixmini-encrypted-password' ]));
set_pconfig ( $uid , 'jappixmini' , 'autosubscribe' , intval ( $b [ 'jappixmini-autosubscribe' ]));
set_pconfig ( $uid , 'jappixmini' , 'autoapprove' , intval ( $b [ 'jappixmini-autoapprove' ]));
set_pconfig ( $uid , 'jappixmini' , 'activate' , intval ( $b [ 'jappixmini-activate' ]));
2012-04-13 15:05:16 +00:00
info ( 'Jappix Mini settings saved.' );
2012-04-14 16:23:42 +00:00
if ( $purge ) {
2012-04-13 15:05:16 +00:00
q ( " DELETE FROM `pconfig` WHERE `uid`= $uid AND `cat`='jappixmini' and `k` LIKE 'id%%' " );
info ( 'List of addresses purged.' );
}
}
}
function jappixmini_script ( & $a , & $s ) {
2012-04-14 16:23:42 +00:00
// adds the script to the page header which starts Jappix Mini
2012-04-13 15:05:16 +00:00
if ( ! local_user ()) return ;
$activate = get_pconfig ( local_user (), 'jappixmini' , 'activate' );
if ( ! $activate ) return ;
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/php/get.php?t=js&g=mini.xml"></script>' . " \r \n " ;
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/php/get.php?t=js&f=presence.js"></script>' . " \r \n " ;
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/php/get.php?t=js&f=caps.js"></script>' . " \r \n " ;
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/php/get.php?t=js&f=name.js"></script>' . " \r \n " ;
2012-04-14 16:23:42 +00:00
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/php/get.php?t=js&f=roster.js"></script>' . " \r \n " ;
2012-04-13 15:05:16 +00:00
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/lib.js"></script>' . " \r \n " ;
$username = get_pconfig ( local_user (), 'jappixmini' , 'username' );
$username = str_replace ( " ' " , " \\ ' " , $username );
$server = get_pconfig ( local_user (), 'jappixmini' , 'server' );
$server = str_replace ( " ' " , " \\ ' " , $server );
$bosh = get_pconfig ( local_user (), 'jappixmini' , 'bosh' );
$bosh = str_replace ( " ' " , " \\ ' " , $bosh );
$encrypted_password = get_pconfig ( local_user (), 'jappixmini' , 'encrypted-password' );
$encrypted_password = str_replace ( " ' " , " \\ ' " , $encrypted_password );
$autoapprove = get_pconfig ( local_user (), 'jappixmini' , 'autoapprove' );
$autoapprove = intval ( $autoapprove );
$autosubscribe = get_pconfig ( local_user (), 'jappixmini' , 'autosubscribe' );
$autosubscribe = intval ( $autosubscribe );
// get a list of jabber accounts of the contacts
$contacts = Array ();
$uid = local_user ();
2012-04-14 16:23:42 +00:00
$rows = q ( " SELECT * FROM `pconfig` WHERE `uid`= $uid AND `cat`='jappixmini' and `k` LIKE 'id%%' " );
2012-04-13 15:05:16 +00:00
foreach ( $rows as $row ) {
2012-04-14 16:23:42 +00:00
$key = $row [ 'k' ];
$pos = strpos ( $key , " : " );
$dfrn_id = substr ( $key , $pos + 1 );
$r = q ( " SELECT `name` FROM `contact` WHERE `uid`= $uid AND `dfrn-id`='%s' OR `issued-id`='%s' " ,
dbesc ( $dfrn_id ),
dbesc ( $dfrn_id )
);
$name = $r [ 0 ][ " name " ];
2012-04-13 15:05:16 +00:00
$value = $row [ 'v' ];
$pos = strpos ( $value , " : " );
$address = substr ( $value , $pos + 1 );
2012-04-14 16:23:42 +00:00
if ( ! $address ) continue ;
if ( ! $name ) $name = $address ;
$contacts [ $address ] = $name ;
2012-04-13 15:05:16 +00:00
}
$contacts_json = json_encode ( $contacts );
2012-04-14 16:23:42 +00:00
// get nickname
$r = q ( " SELECT `username` FROM `user` WHERE `uid`= $uid " );
$nickname = json_encode ( $r [ 0 ][ " username " ]);
// add javascript to start Jappix Mini
2012-04-13 15:05:16 +00:00
$a -> page [ 'htmlhead' ] .= " <script type= \" text/javascript \" >
jQuery ( document ) . ready ( function () {
2012-04-14 16:23:42 +00:00
jappixmini_addon_start ( '$server' , '$username' , '$bosh' , '$encrypted_password' , $nickname );
2012-04-13 15:05:16 +00:00
jappixmini_manage_roster ( $contacts_json , $autoapprove , $autosubscribe );
});
</ script > " ;
return ;
}
function jappixmini_login ( & $a , & $o ) {
2012-04-14 16:23:42 +00:00
// for setDB, needed by jappixmini_addon_set_client_secret
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/php/get.php?t=js&f=datastore.js"></script>' . " \r \n " ;
// for str_sha1, needed by jappixmini_addon_set_client_secret
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/php/get.php?t=js&f=jsjac.js"></script>' . " \r \n " ;
// for jappixmini_addon_set_client_secret
$a -> page [ 'htmlhead' ] .= '<script type="text/javascript" src="' . $a -> get_baseurl () . '/addon/jappixmini/lib.js"></script>' . " \r \n " ;
// save hash of password
2012-04-13 15:05:16 +00:00
$o = str_replace ( " <form " , " <form onsubmit= \" jappixmini_addon_set_client_secret(this.elements['id_password'].value);return true; \" " , $o );
}
function jappixmini_cron ( & $a , $d ) {
// For autosubscribe/autoapprove, we need to maintain a list of jabber addresses of our contacts.
// 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' " );
foreach ( $users as $row ) {
$uid = $row [ " uid " ];
// for each user, go through list of contacts
$contacts = q ( " SELECT * FROM `contact` WHERE `uid`=%d AND ((LENGTH(`dfrn-id`) AND LENGTH(`pubkey`)) OR (LENGTH(`issued-id`) AND LENGTH(`prvkey`))) " , intval ( $uid ));
foreach ( $contacts as $contact_row ) {
$request = $contact_row [ " request " ];
if ( ! $request ) continue ;
$dfrn_id = $contact_row [ " dfrn-id " ];
2012-04-14 16:23:42 +00:00
if ( $dfrn_id ) {
$key = $contact_row [ " pubkey " ];
$encrypt_func = openssl_public_encrypt ;
$decrypt_func = openssl_public_decrypt ;
$role = " prv " ;
} else {
2012-04-13 15:05:16 +00:00
$dfrn_id = $contact_row [ " issued-id " ];
2012-04-14 16:23:42 +00:00
$key = $contact_row [ " prvkey " ];
$encrypt_func = openssl_private_encrypt ;
$decrypt_func = openssl_private_decrypt ;
$role = " pub " ;
2012-04-13 15:05:16 +00:00
}
// check if jabber address already present
$present = get_pconfig ( $uid , " jappixmini " , " id: " . $dfrn_id );
$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
if ( $now - $timestamp < 3600 * 24 * 7 ) continue ;
}
// construct base retrieval address
$pos = strpos ( $request , " /dfrn_request/ " );
if ( $pos === false ) continue ;
2012-04-14 16:23:42 +00:00
$base = substr ( $request , 0 , $pos ) . " /jappixmini?role= $role " ;
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
// construct own address
$username = get_pconfig ( $uid , 'jappixmini' , 'username' );
if ( ! $username ) continue ;
$server = get_pconfig ( $uid , 'jappixmini' , 'server' );
if ( ! $server ) continue ;
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
$address = $username . " @ " . $server ;
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
// sign address
$signed_address = " " ;
$encrypt_func ( $address , $signed_address , $key );
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
// construct request url
$signed_address_hex = bin2hex ( $signed_address );
$url = $base . " &signed_address= $signed_address_hex &dfrn_id= " . urlencode ( $dfrn_id );
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
try {
// send request
$answer_json = fetch_url ( $url );
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
// parse answer
2012-04-13 15:05:16 +00:00
$answer = json_decode ( $answer_json );
2012-04-14 16:23:42 +00:00
if ( $answer -> status != " ok " ) throw new Exception ();
$encrypted_address_hex = $answer -> encrypted_address ;
if ( ! $encrypted_address_hex ) throw new Exception ();
$encrypted_address = hex2bin ( $encrypted_address_hex );
if ( ! $encrypted_address ) throw new Exception ();
2012-04-13 15:05:16 +00:00
2012-04-14 16:23:42 +00:00
// decrypt address
$decrypted_address = " " ;
$decrypt_func ( $encrypted_address , $decrypted_address , $key );
if ( ! $decrypted_address ) throw new Exception ();
} catch ( Exception $e ) {
$decrypted_address = " " ;
2012-04-13 15:05:16 +00:00
}
// save address
2012-04-14 16:23:42 +00:00
set_pconfig ( $uid , " jappixmini " , " id: $dfrn_id " , " $now : $decrypted_address " );
2012-04-13 15:05:16 +00:00
}
}
}
function jappixmini_download_source ( & $a , & $b ) {
2012-04-14 16:23:42 +00:00
// Jappix Mini source download link on About page
2012-04-13 15:05:16 +00:00
$b .= '<h1>Jappix Mini</h1>' ;
$b .= '<p>This site uses Jappix Mini by the <a href="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/AUTHORS">Jappix authors</a>, which is distributed under the terms of the <a href="' . $a -> get_baseurl () . '/addon/jappixmini/jappix/COPYING">GNU Affero General Public License</a>.</p>' ;
$b .= '<p>You can download the <a href="' . $a -> get_baseurl () . '/addon/jappixmini/jappix.zip">source code</a>.</p>' ;
}