diff --git a/README b/README deleted file mode 100644 index d8abf2a0..00000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -Repository for Friendika addon modules diff --git a/calc.tgz b/calc.tgz new file mode 100644 index 00000000..f5d6870d Binary files /dev/null and b/calc.tgz differ diff --git a/calc/calc.php b/calc/calc.php new file mode 100644 index 00000000..8c079dc7 --- /dev/null +++ b/calc/calc.php @@ -0,0 +1,363 @@ + + */ + + +function calc_install() { + register_hook('app_menu', 'addon/calc/calc.php', 'calc_app_menu'); +} + +function calc_uninstall() { + unregister_hook('app_menu', 'addon/calc/calc.php', 'calc_app_menu'); + +} + +function calc_app_menu($a,&$b) { + $b['app_menu'] .= '
Calculator
'; +} + + +function calc_module() {} + + + + +function calc_init($a) { + +$x = <<< EOT + + + +EOT; +$a->page['htmlhead'] .= $x; +} + +function calc_content($app) { + +$o = ''; + +$o .= <<< EOT + +

Calculator

+

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + +
+ +EOT; +return $o; + +} diff --git a/convert.tgz b/convert.tgz new file mode 100644 index 00000000..b6e3e2a5 Binary files /dev/null and b/convert.tgz differ diff --git a/convert/UnitConvertor.php b/convert/UnitConvertor.php new file mode 100644 index 00000000..d7933a8f --- /dev/null +++ b/convert/UnitConvertor.php @@ -0,0 +1,283 @@ + | +// | Co-authored by : CVH, Chris Hansel | +// +----------------------------------------------------------------------+ +// +// $Id: UnitConvertor.php,v 1.00 2002/02/20 11:40:00 stasokhvat Exp $ + +/** +* UnitConvertor is able to convert between different units and currencies. +* +* @author Stanislav Okhvat +* @version $Id: UnitConvertor.php,v 1.00 2002/03/01 17:00:00 stasokhvat Exp $ +* @package UnitConvertor +* @access public +* @history 01.03.2002 Implemented the code for regular and offset-based +* conversions +* +* 13.12.2004 +* By Chris Hansel (CVH): changed getConvSpecs in order to have it look up +* intermediary conversions (also see comments in check_key). +* +* Intermediary conversions are useful when no conversion ratio is specified +* between two units when we calculate between the two. For example, we want +* to convert between Fahrenheit and Kelvin, and we have only +* specified how to convert Centigrade<->Fahrenheit and +* Centigrade<->Kelvin. While a direct (Fahrenheit->Kelvin) or +* reverse (Kelvin->Fahrenheit) lookups fail, looking for an intermediary +* unit linking the two (Centigrade) helps us do the conversion. +* +* 13.12.2004 +* Chris Hansel (CVH): $to_array argument of addConversion method can now +* contain units as 'unit1/unit2/unit3', when all units stand for the same +* thing. See examples in unitconv.php +*/ +class UnitConvertor +{ + /** + * Stores conversion ratios. + * + * @var array + * @access private + */ + var $conversion_table = array(); + + /** + * Decimal point character (default is "." - American - set in constructor). + * + * @var string + * @access private + */ + var $decimal_point; + + /** + * Thousands separator (default is "," - American - set in constructor). + * + * @var string + * @access private + */ + var $thousand_separator; + + /** + * For future use + * + * @var array + * @access private + */ + var $bases = array(); + + /** + * Constructor. Initializes the UnitConvertor object with the most important + * properties. + * + * @param string decimal point character + * @param string thousand separator character + * @return void + * @access public + */ + function UnitConvertor($dec_point = '.', $thousand_sep = ',') + { + $this->decimal_point = $dec_point; + $this->thousand_separator = $thousand_sep; + + } // end func UnitConvertor + + /** + * Adds a conversion ratio to the conversion table. + * + * @param string the name of unit from which to convert + * @param array array( + * "pound"=>array("ratio"=>'', "offset"=>'') + * ) + * "pound" - name of unit to set conversion ration to + * "ratio" - 'double' conversion ratio which, when + * multiplied by the number of $from_unit units produces + * the result + * "offset" - an offset from 0 which will be added to + * the result when converting (needed for temperature + * conversions and defaults to 0). + * @return boolean true if successful, false otherwise + * @access public + */ + function addConversion($from_unit, $to_array) + { + if (!isset($this->conversion_table[$from_unit])) { + while(list($key, $val) = each($to_array)) + { + if (strstr($key, '/')) + { + $to_units = explode('/', $key); + foreach ($to_units as $to_unit) + { + $this->bases[$from_unit][] = $to_unit; + + if (!is_array($val)) + { + $this->conversion_table[$from_unit."_".$to_unit] = array("ratio"=>$val, "offset"=>0); + } + else + { + $this->conversion_table[$from_unit."_".$to_unit] = + array( + "ratio"=>$val['ratio'], + "offset"=>(isset($val['offset']) ? $val['offset'] : 0) + ); + } + } + } + else + { + $this->bases[$from_unit][] = $key; + + if (!is_array($val)) + { + $this->conversion_table[$from_unit."_".$key] = array("ratio"=>$val, "offset"=>0); + } + else + { + $this->conversion_table[$from_unit."_".$key] = + array( + "ratio"=>$val['ratio'], + "offset"=>(isset($val['offset']) ? $val['offset'] : 0) + ); + } + } + } + return true; + } + return false; + + } // end func addConversion + + /** + * Converts from one unit to another using specified precision. + * + * @param double value to convert + * @param string name of the source unit from which to convert + * @param string name of the target unit to which we are converting + * @param integer double precision of the end result + * @return void + * @access public + */ + function convert($value, $from_unit, $to_unit, $precision) + { + if ($this->getConvSpecs($from_unit, $to_unit, $value, $converted )) + { + return number_format($converted , (int)$precision, $this->decimal_point, $this->thousand_separator); + } else { + return false; + } + } // end func + + /** + * CVH : changed this Function getConvSpecs in order to have it look up + * intermediary Conversions from the + * "base" unit being that one that has the highest hierarchical order in one + * "logical" Conversion_Array + * when taking $conv->addConversion('km', + * array('meter'=>1000, 'dmeter'=>10000, 'centimeter'=>100000, + * 'millimeter'=>1000000, 'mile'=>0.62137, 'naut.mile'=>0.53996, + * 'inch(es)/zoll'=>39370, 'ft/foot/feet'=>3280.8, 'yd/yard'=>1093.6)); + * "km" would be the logical base unit for all units of dinstance, thus, + * if the function fails to find a direct or reverse conversion in the table + * it is only logical to suspect that if there is a chance + * converting the value it only is via the "base" unit, and so + * there is not even a need for a recursive search keeping the perfomance + * acceptable and the ressource small... + * + * CVH check_key checks for a key in the Conversiontable and returns a value + */ + function check_key( $key) { + if ( array_key_exists ($key,$this->conversion_table)) { + if (! empty($this->conversion_table[$key])) { + return $this->conversion_table[$key]; + } + } + return false; + } + + /** + * Key function. Finds the conversion ratio and offset from one unit to another. + * + * @param string name of the source unit from which to convert + * @param string name of the target unit to which we are converting + * @param double conversion ratio found. Returned by reference. + * @param double offset which needs to be added (or subtracted, if negative) + * to the result to convert correctly. + * For temperature or some scientific conversions, + * i.e. Fahrenheit -> Celcius + * @return boolean true if ratio and offset are found for the supplied + * units, false otherwise + * @access private + */ + function getConvSpecs($from_unit, $to_unit, $value, &$converted) + { + $key = $from_unit."_".$to_unit; + $revkey = $to_unit."_".$from_unit; + $found = false; + if ($ct_arr = $this->check_key($key)) { + // Conversion Specs found directly + $ratio = (double)$ct_arr['ratio']; + $offset = $ct_arr['offset']; + $converted = (double)(($value * $ratio)+ $offset); + + return true; + } // not found in direct order, try reverse order + elseif ($ct_arr = $this->check_key($revkey)) { + $ratio = (double)(1/$ct_arr['ratio']); + $offset = -$ct_arr['offset']; + $converted = (double)(($value + $offset) * $ratio); + + return true; + } // not found test for intermediary conversion + else { + // return ratio = 1 if keyparts match + if ($key == $revkey) { + $ratio = 1; + $offset = 0; + $converted = $value; + return true; + } + // otherwise search intermediary + reset($this->conversion_table); + while (list($convk, $i1_value) = each($this->conversion_table)) { + // split the key into parts + $keyparts = preg_split("/_/",$convk); + // return ratio = 1 if keyparts match + + // Now test if either part matches the from or to unit + if ($keyparts[1] == $to_unit && ($i2_value = $this->check_key($keyparts[0]."_".$from_unit))) { + // an intermediary $keyparts[0] was found + // now let us put things together intermediary 1 and 2 + $converted = (double)(((($value - $i2_value['offset']) / $i2_value['ratio']) * $i1_value['ratio'])+ $i1_value['offset']); + + $found = true; + + } elseif ($keyparts[1] == $from_unit && ($i2_value = $this->check_key($keyparts[0]."_".$to_unit))) { + // an intermediary $keyparts[0] was found + // now let us put things together intermediary 2 and 1 + $converted = (double)(((($value - $i1_value['offset']) / $i1_value['ratio']) + $i2_value['offset']) * $i2_value['ratio']); + + $found = true; + } + } + return $found; + } + + } // end func getConvSpecs + +} // end class UnitConvertor +?> \ No newline at end of file diff --git a/convert/convert.php b/convert/convert.php new file mode 100644 index 00000000..7a4c90a5 --- /dev/null +++ b/convert/convert.php @@ -0,0 +1,228 @@ + + */ + +function convert_install() { + register_hook('app_menu', 'addon/convert/convert.php', 'convert_app_menu'); +} + +function convert_uninstall() { + unregister_hook('app_menu', 'addon/convert/convert.php', 'convert_app_menu'); +} + +function convert_app_menu($a,&$b) { + $b['app_menu'] .= ''; +} + + +function convert_module() {} + + + + + + + +function convert_content($app) { + +include("UnitConvertor.php"); + + class TP_Converter extends UnitConvertor { + function TP_Converter($lang = "en") + { + if ($lang != 'en' ) { + $dec_point = '.'; $thousand_sep = "'"; + } else { + $dec_point = '.'; $thousand_sep = ","; + } + + $this->UnitConvertor($dec_point , $thousand_sep ); + + } // end func UnitConvertor + + function find_base_unit($from,$to) { + while (list($skey,$sval) = each($this->bases)) { + if ($skey == $from || $to == $skey || in_array($to,$sval) || in_array($from,$sval)) { + return $skey; + } + } + return false; + } + + function getTable($value, $from_unit, $to_unit, $precision) { + + if ($base_unit = $this->find_base_unit($from_unit,$to_unit)) { + + // A baseunit was found now lets convert from -> $base_unit + + $cell ['value'] = $this->convert($value, $from_unit, $base_unit, $precision)." ".$base_unit; + $cell ['class'] = ($base_unit == $from_unit || $base_unit == $to_unit) ? "framedred": ""; + $cells[] = $cell; + // We now have the base unit and value now lets produce the table; + while (list($key,$val) = each($this->bases[$base_unit])) { + $cell ['value'] = $this->convert($value, $from_unit, $val, $precision)." ".$val; + $cell ['class'] = ($val == $from_unit || $val == $to_unit) ? "framedred": ""; + $cells[] = $cell; + } + + $cc = count($cells); + $string = ""; + $string .= ""; + $i=0; + foreach ($cells as $cell) { + if ($i==0) { + $string .= ""; + $i++; + } else { + $string .= ""; + } + } + $string .= "
$value $from_unit".$cell['value']."
".$cell['value']."
"; + return $string; + } + + } +} + + +$conv = new TP_Converter('en'); + + +$conversions = array( + 'Temperature'=>array('base' =>'Celsius', + 'conv'=>array( + 'Fahrenheit'=>array('ratio'=>1.8, 'offset'=>32), + 'Kelvin'=>array('ratio'=>1, 'offset'=>273), + 'Reaumur'=>0.8 + ) + ), + 'Weight' => array('base' =>'kg', + 'conv'=>array( + 'g'=>1000, + 'mg'=>1000000, + 't'=>0.001, + 'grain'=>15432, + 'oz'=>35.274, + 'lb'=>2.2046, + 'cwt(UK)' => 0.019684, + 'cwt(US)' => 0.022046, + 'ton (US)' => 0.0011023, + 'ton (UK)' => 0.0009842 + ) + ), + 'Distance' => array('base' =>'km', + 'conv'=>array( + 'm'=>1000, + 'dm'=>10000, + 'cm'=>100000, + 'mm'=>1000000, + 'mile'=>0.62137, + 'naut.mile'=>0.53996, + 'inch(es)'=>39370, + 'ft'=>3280.8, + 'yd'=>1093.6, + 'furlong'=>4.970969537898672, + 'fathom'=>546.8066491688539 + ) + ), + 'Area' => array('base' =>'km 2', + 'conv'=>array( + 'ha'=>100, + 'acre'=>247.105, + 'm 2'=>pow(1000,2), + 'dm 2'=>pow(10000,2), + 'cm 2'=>pow(100000,2), + 'mm 2'=>pow(1000000,2), + 'mile 2'=>pow(0.62137,2), + 'naut.miles 2'=>pow(0.53996,2), + 'in 2'=>pow(39370,2), + 'ft 2'=>pow(3280.8,2), + 'yd 2'=>pow(1093.6,2), + ) + ), + 'Volume' => array('base' =>'m 3', + 'conv'=>array( + 'in 3'=>61023.6, + 'ft 3'=>35.315, + 'cm 3'=>pow(10,6), + 'dm 3'=>1000, + 'litre'=>1000, + 'hl'=>10, + 'yd 3'=>1.30795, + 'gal(US)'=>264.172, + 'gal(UK)'=>219.969, + 'pint' => 2113.376, + 'quart' => 1056.688, + 'cup' => 4266.753, + 'fl oz' => 33814.02, + 'tablespoon' => 67628.04, + 'teaspoon' => 202884.1, + 'pt (UK)'=>1000/0.56826, + 'barrel petroleum'=>1000/158.99, + 'Register Tons'=>2.832, + 'Ocean Tons'=>1.1327 + ) + ), + 'Speed' =>array('base' =>'kmph', + 'conv'=>array( + 'mps'=>0.0001726031, + 'milesph'=>0.62137, + 'knots'=>0.53996, + 'mach STP'=>0.0008380431, + 'c (warp)'=>9.265669e-10 + ) + ) +); + + +while (list($key,$val) = each($conversions)) { + $conv->addConversion($val['base'], $val['conv']); + $list[$key][] = $val['base']; + while (list($ukey,$uval) = each($val['conv'])) { + $list[$key][] = $ukey; + } +} + + $o .= '

Unit Conversions

'; + + + if (isset($_POST['from_unit']) && isset($_POST['value'])) { + $_POST['value'] = $_POST['value'] + 0; + + + $o .= ($conv->getTable($_POST['value'], $_POST['from_unit'], $_POST['to_unit'], 5))."

"; + } else { + $o .= "

Select:

"; + } + + if(isset($_POST['value'])) + $value = $_POST['value']; + else + $value = ''; + + $o .= '
'; + $o .= ''; + $o .= ''; + + $o .= '
'; + + return $o; +} diff --git a/facebook.tgz b/facebook.tgz new file mode 100644 index 00000000..05c7c735 Binary files /dev/null and b/facebook.tgz differ diff --git a/facebook/README b/facebook/README new file mode 100644 index 00000000..325f18dd --- /dev/null +++ b/facebook/README @@ -0,0 +1,39 @@ +Installing the Friendika/Facebook connector + +1. register an API key for your site from developer.facebook.com + a. We'd be very happy if you include "Friendika" in the application name + to increase name recognition. The Friendika icons are also present + in the images directory and may be uploaded as a Facebook app icon. + Use images/friendika-16.jpg for the Icon and images/friendika-128.jpg for the Logo. + b. The url should be your site URL with a trailing slash. + You may use http://portal.friendika.com/privacy as the privacy policy + URL unless your site has different requirements, and + http://portal.friendika.com as the Terms of Service URL unless + you have different requirements. (Friendika is a software application + and does not require Terms of Service, though your installation of it might). + c. Set the following values in your .htconfig.php file + $a->config['facebook']['appid'] = 'xxxxxxxxxxx'; + $a->config['facebook']['appsecret'] = 'xxxxxxxxxxxxxxx'; + Replace with the settings Facebook gives you. + d. Navigate to Set Web->Site URL & Domain -> Website Settings. Set Site URL + to yoursubdomain.yourdomain.com. Set Site Domain to your yourdomain.com. +2. Enable the facebook plugin by including it in .htconfig.php - e.g. + $a->config['system']['addon'] = 'plugin1,plugin2,facebook'; +3. Visit the Facebook Settings section of the "Settings->Plugin Settings" page. + and click 'Install Facebook Connector'. +4. This will ask you to login to Facebook and grant permission to the + plugin to do its stuff. Allow it to do so. +5. You're done. To turn it off visit the Plugin Settings page again and + 'Remove Facebook posting'. + +Vidoes and embeds will not be posted if there is no other content. Links +and images will be converted to a format suitable for the Facebook API and +long posts truncated - with a link to view the full post. + +Facebook contacts will not be able to view private photos, as they are not able to +authenticate to your site to establish identity. We will address this +in a future release. + +Info: please make sure that you understand all aspects due to Friendika's +default licence which is: Creative Commons Attribution 3.0 (further info: +http://creativecommons.org/licenses/by/3.0/ ) diff --git a/facebook/facebook.css b/facebook/facebook.css new file mode 100644 index 00000000..0c164331 --- /dev/null +++ b/facebook/facebook.css @@ -0,0 +1,13 @@ + +#facebook-enable-wrapper { + margin-top: 20px; +} + +#facebook-disable-wrapper { + margin-top: 20px; +} + +#facebook-post-default-form input { + margin-top: 20px; + margin-right: 20px; +} \ No newline at end of file diff --git a/facebook/facebook.php b/facebook/facebook.php new file mode 100644 index 00000000..7ffdaffa --- /dev/null +++ b/facebook/facebook.php @@ -0,0 +1,1059 @@ + + */ + +/** + * Installing the Friendika/Facebook connector + * + * 1. register an API key for your site from developer.facebook.com + * a. We'd be very happy if you include "Friendika" in the application name + * to increase name recognition. The Friendika icons are also present + * in the images directory and may be uploaded as a Facebook app icon. + * Use images/friendika-16.jpg for the Icon and images/friendika-128.jpg for the Logo. + * b. The url should be your site URL with a trailing slash. + * You may use http://portal.friendika.com/privacy as the privacy policy + * URL unless your site has different requirements, and + * http://portal.friendika.com as the Terms of Service URL unless + * you have different requirements. (Friendika is a software application + * and does not require Terms of Service, though your installation of it might). + * c. Set the following values in your .htconfig.php file + * $a->config['facebook']['appid'] = 'xxxxxxxxxxx'; + * $a->config['facebook']['appsecret'] = 'xxxxxxxxxxxxxxx'; + * Replace with the settings Facebook gives you. + * d. Navigate to Set Web->Site URL & Domain -> Website Settings. Set + * Site URL to yoursubdomain.yourdomain.com. Set Site Domain to your + * yourdomain.com. + * 2. Enable the facebook plugin by including it in .htconfig.php - e.g. + * $a->config['system']['addon'] = 'plugin1,plugin2,facebook'; + * 3. Visit the Facebook Settings section of the "Settings->Plugin Settings" page. + * and click 'Install Facebook Connector'. + * 4. This will ask you to login to Facebook and grant permission to the + * plugin to do its stuff. Allow it to do so. + * 5. You're done. To turn it off visit the Plugin Settings page again and + * 'Remove Facebook posting'. + * + * Vidoes and embeds will not be posted if there is no other content. Links + * and images will be converted to a format suitable for the Facebook API and + * long posts truncated - with a link to view the full post. + * + * Facebook contacts will not be able to view private photos, as they are not able to + * authenticate to your site to establish identity. We will address this + * in a future release. + */ + +define('FACEBOOK_MAXPOSTLEN', 420); + + +function facebook_install() { + register_hook('post_local_end', 'addon/facebook/facebook.php', 'facebook_post_hook'); + register_hook('jot_networks', 'addon/facebook/facebook.php', 'facebook_jot_nets'); + register_hook('plugin_settings', 'addon/facebook/facebook.php', 'facebook_plugin_settings'); + register_hook('cron', 'addon/facebook/facebook.php', 'facebook_cron'); + register_hook('queue_predeliver', 'addon/facebook/facebook.php', 'fb_queue_hook'); +} + + +function facebook_uninstall() { + unregister_hook('post_local_end', 'addon/facebook/facebook.php', 'facebook_post_hook'); + unregister_hook('jot_networks', 'addon/facebook/facebook.php', 'facebook_jot_nets'); + unregister_hook('plugin_settings', 'addon/facebook/facebook.php', 'facebook_plugin_settings'); + unregister_hook('cron', 'addon/facebook/facebook.php', 'facebook_cron'); + unregister_hook('queue_predeliver', 'addon/facebook/facebook.php', 'fb_queue_hook'); +} + + +/* declare the facebook_module function so that /facebook url requests will land here */ + +function facebook_module() {} + + + +/* If a->argv[1] is a nickname, this is a callback from Facebook oauth requests. */ + +function facebook_init(&$a) { + + if($a->argc != 2) + return; + $nick = $a->argv[1]; + if(strlen($nick)) + $r = q("SELECT `uid` FROM `user` WHERE `nickname` = '%s' LIMIT 1", + dbesc($nick) + ); + if(! count($r)) + return; + + $uid = $r[0]['uid']; + $auth_code = (($_GET['code']) ? $_GET['code'] : ''); + $error = (($_GET['error_description']) ? $_GET['error_description'] : ''); + + + if($error) + logger('facebook_init: Error: ' . $error); + + if($auth_code && $uid) { + + $appid = get_config('facebook','appid'); + $appsecret = get_config('facebook', 'appsecret'); + + $x = fetch_url('https://graph.facebook.com/oauth/access_token?client_id=' + . $appid . '&client_secret=' . $appsecret . '&redirect_uri=' + . urlencode($a->get_baseurl() . '/facebook/' . $nick) + . '&code=' . $auth_code); + + logger('facebook_init: returned access token: ' . $x, LOGGER_DATA); + + if(strpos($x,'access_token=') !== false) { + $token = str_replace('access_token=', '', $x); + if(strpos($token,'&') !== false) + $token = substr($token,0,strpos($token,'&')); + set_pconfig($uid,'facebook','access_token',$token); + set_pconfig($uid,'facebook','post','1'); + if(get_pconfig($uid,'facebook','no_linking') === false) + set_pconfig($uid,'facebook','no_linking',1); + fb_get_self($uid); + fb_get_friends($uid); + fb_consume_all($uid); + + } + + } + +} + + +function fb_get_self($uid) { + $access_token = get_pconfig($uid,'facebook','access_token'); + if(! $access_token) + return; + $s = fetch_url('https://graph.facebook.com/me/?access_token=' . $access_token); + if($s) { + $j = json_decode($s); + set_pconfig($uid,'facebook','self_id',(string) $j->id); + } +} + + + +function fb_get_friends($uid) { + + $r = q("SELECT `id` FROM `user` WHERE `uid` = %d AND `account_expired` = 0 LIMIT 1", + intval($uid) + ); + if(! count($r)) + return; + + $access_token = get_pconfig($uid,'facebook','access_token'); + + $no_linking = get_pconfig($uid,'facebook','no_linking'); + if($no_linking) + return; + + if(! $access_token) + return; + $s = fetch_url('https://graph.facebook.com/me/friends?access_token=' . $access_token); + if($s) { + logger('facebook: fb_get_friends: ' . $s, LOGGER_DATA); + $j = json_decode($s); + logger('facebook: fb_get_friends: json: ' . print_r($j,true), LOGGER_DATA); + if(! $j->data) + return; + foreach($j->data as $person) { + $s = fetch_url('https://graph.facebook.com/' . $person->id . '?access_token=' . $access_token); + if($s) { + $jp = json_decode($s); + logger('fb_get_friends: info: ' . print_r($jp,true), LOGGER_DATA); + + // always use numeric link for consistency + + $jp->link = 'http://facebook.com/profile.php?id=' . $person->id; + + // check if we already have a contact + + $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `url` = '%s' LIMIT 1", + intval($uid), + dbesc($jp->link) + ); + + if(count($r)) { + + // check that we have all the photos, this has been known to fail on occasion + + if((! $r[0]['photo']) || (! $r[0]['thumb']) || (! $r[0]['micro'])) { + require_once("Photo.php"); + + $photos = import_profile_photo('https://graph.facebook.com/' . $jp->id . '/picture', $uid, $r[0]['id']); + + $r = q("UPDATE `contact` SET `photo` = '%s', + `thumb` = '%s', + `micro` = '%s', + `name-date` = '%s', + `uri-date` = '%s', + `avatar-date` = '%s' + WHERE `id` = %d LIMIT 1 + ", + dbesc($photos[0]), + dbesc($photos[1]), + dbesc($photos[2]), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + intval($r[0]['id']) + ); + } + continue; + } + else { + + // create contact record + $r = q("INSERT INTO `contact` ( `uid`, `created`, `url`, `addr`, `alias`, `notify`, `poll`, + `name`, `nick`, `photo`, `network`, `rel`, `priority`, + `writable`, `blocked`, `readonly`, `pending` ) + VALUES ( %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, 0, 0, 0 ) ", + intval($uid), + dbesc(datetime_convert()), + dbesc($jp->link), + dbesc(''), + dbesc(''), + dbesc($jp->id), + dbesc('facebook ' . $jp->id), + dbesc($jp->name), + dbesc(($jp->nickname) ? $jp->nickname : strtolower($jp->first_name)), + dbesc('https://graph.facebook.com/' . $jp->id . '/picture'), + dbesc(NETWORK_FACEBOOK), + intval(CONTACT_IS_FRIEND), + intval(1), + intval(1) + ); + } + + $r = q("SELECT * FROM `contact` WHERE `url` = '%s' AND `uid` = %d LIMIT 1", + dbesc($jp->link), + intval($uid) + ); + + if(! count($r)) { + continue; + } + + $contact = $r[0]; + $contact_id = $r[0]['id']; + + require_once("Photo.php"); + + $photos = import_profile_photo($r[0]['photo'],$uid,$contact_id); + + $r = q("UPDATE `contact` SET `photo` = '%s', + `thumb` = '%s', + `micro` = '%s', + `name-date` = '%s', + `uri-date` = '%s', + `avatar-date` = '%s' + WHERE `id` = %d LIMIT 1 + ", + dbesc($photos[0]), + dbesc($photos[1]), + dbesc($photos[2]), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + intval($contact_id) + ); + + } + } + } +} + +// This is the POST method to the facebook settings page +// Content is posted to Facebook in the function facebook_post_hook() + +function facebook_post(&$a) { + + $uid = local_user(); + if($uid){ + + $value = ((x($_POST,'post_by_default')) ? intval($_POST['post_by_default']) : 0); + set_pconfig($uid,'facebook','post_by_default', $value); + + $no_linking = get_pconfig($uid,'facebook','no_linking'); + + $no_wall = ((x($_POST,'facebook_no_wall')) ? intval($_POST['facebook_no_wall']) : 0); + set_pconfig($uid,'facebook','no_wall',$no_wall); + + $private_wall = ((x($_POST,'facebook_private_wall')) ? intval($_POST['facebook_private_wall']) : 0); + set_pconfig($uid,'facebook','private_wall',$private_wall); + + + $linkvalue = ((x($_POST,'facebook_linking')) ? intval($_POST['facebook_linking']) : 0); + set_pconfig($uid,'facebook','no_linking', (($linkvalue) ? 0 : 1)); + + // FB linkage was allowed but has just been turned off - remove all FB contacts and posts + + if((! intval($no_linking)) && (! intval($linkvalue))) { + $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `network` = '%s' ", + intval($uid), + dbesc(NETWORK_FACEBOOK) + ); + if(count($r)) { + require_once('include/Contact.php'); + foreach($r as $rr) + contact_remove($rr['id']); + } + } + elseif(intval($no_linking) && intval($linkvalue)) { + // FB linkage is now allowed - import stuff. + fb_get_self($uid); + fb_get_friends($uid); + fb_consume_all($uid); + } + + info( t('Settings updated.') . EOL); + } + + return; +} + +// Facebook settings form + +function facebook_content(&$a) { + + if(! local_user()) { + notice( t('Permission denied.') . EOL); + return ''; + } + + if($a->argc > 1 && $a->argv[1] === 'remove') { + del_pconfig(local_user(),'facebook','post'); + info( t('Facebook disabled') . EOL); + } + + if($a->argc > 1 && $a->argv[1] === 'friends') { + fb_get_friends(local_user()); + info( t('Updating contacts') . EOL); + } + + + $fb_installed = get_pconfig(local_user(),'facebook','post'); + + $appid = get_config('facebook','appid'); + + if(! $appid) { + notice( t('Facebook API key is missing.') . EOL); + return ''; + } + + $a->page['htmlhead'] .= '' . "\r\n"; + + $o .= '

' . t('Facebook Connect') . '

'; + + if(! $fb_installed) { + $o .= ''; + } + + if($fb_installed) { + $o .= ''; + + $o .= ''; + + $o .= '
'; + $o .= '
'; + $post_by_default = get_pconfig(local_user(),'facebook','post_by_default'); + $checked = (($post_by_default) ? ' checked="checked" ' : ''); + $o .= '' . ' ' . t('Post to Facebook by default') . EOL; + + $no_linking = get_pconfig(local_user(),'facebook','no_linking'); + $checked = (($no_linking) ? '' : ' checked="checked" '); + $o .= '' . ' ' . t('Link all your Facebook friends and conversations on this website') . EOL ; + + $o .= '

' . t('Facebook conversations consist of your profile wall and your friend stream.'); + $o .= ' ' . t('On this website, your Facebook friend stream is only visible to you.'); + $o .= ' ' . t('The following settings determine the privacy of your Facebook profile wall on this website.') . '

'; + + $private_wall = get_pconfig(local_user(),'facebook','private_wall'); + $checked = (($private_wall) ? ' checked="checked" ' : ''); + $o .= '' . ' ' . t('On this website your Facebook profile wall conversations will only be visible to you') . EOL ; + + + $no_wall = get_pconfig(local_user(),'facebook','no_wall'); + $checked = (($no_wall) ? ' checked="checked" ' : ''); + $o .= '' . ' ' . t('Do not import your Facebook profile wall conversations') . EOL ; + + $o .= '

' . t('If you choose to link conversations and leave both of these boxes unchecked, your Facebook profile wall will be merged with your profile wall on this website and your privacy settings on this website will be used to determine who may see the conversations.') . '

'; + + $o .= '
'; + } + + return $o; +} + + + +function facebook_cron($a,$b) { + + $last = get_config('facebook','last_poll'); + + $poll_interval = intval(get_config('facebook','poll_interval')); + if(! $poll_interval) + $poll_interval = 3600; + + if($last) { + $next = $last + $poll_interval; + if($next > time()) + return; + } + + logger('facebook_cron'); + + + // Find the FB users on this site and randomize in case one of them + // uses an obscene amount of memory. It may kill this queue run + // but hopefully we'll get a few others through on each run. + + $r = q("SELECT * FROM `pconfig` WHERE `cat` = 'facebook' AND `k` = 'post' AND `v` = '1' ORDER BY RAND() "); + if(count($r)) { + foreach($r as $rr) { + if(get_pconfig($rr['uid'],'facebook','no_linking')) + continue; + // check for new friends once a day + $last_friend_check = get_pconfig($rr['uid'],'facebook','friend_check'); + if($last_friend_check) + $next_friend_check = $last_friend_check + 86400; + if($next_friend_check <= time()) { + fb_get_friends($rr['uid']); + set_pconfig($rr['uid'],'facebook','friend_check',time()); + } + fb_consume_all($rr['uid']); + } + } + + set_config('facebook','last_poll', time()); + +} + + + +function facebook_plugin_settings(&$a,&$b) { + + $b .= '
'; + $b .= '

' . t('Facebook') . '

'; + $b .= '' . t('Facebook Connector Settings') . '
'; + $b .= '
'; + +} + +function facebook_jot_nets(&$a,&$b) { + if(! local_user()) + return; + + $fb_post = get_pconfig(local_user(),'facebook','post'); + if(intval($fb_post) == 1) { + $fb_defpost = get_pconfig(local_user(),'facebook','post_by_default'); + $selected = ((intval($fb_defpost) == 1) ? ' checked="checked" ' : ''); + $b .= '
' + . t('Post to Facebook') . '
'; + } +} + + +function facebook_post_hook(&$a,&$b) { + + /** + * Post to Facebook stream + */ + + require_once('include/group.php'); + + logger('Facebook post'); + + $reply = false; + $likes = false; + + if((local_user()) && (local_user() == $b['uid'])) { + + // Facebook is not considered a private network + if($b['prvnets'] && $b['private']) + return; + + $linking = ((get_pconfig(local_user(),'facebook','no_linking')) ? 0 : 1); + + if(($b['parent']) && ($linking)) { + $r = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($b['parent']), + intval(local_user()) + ); + if(count($r) && substr($r[0]['uri'],0,4) === 'fb::') + $reply = substr($r[0]['uri'],4); + elseif(count($r) && substr($r[0]['extid'],0,4) === 'fb::') + $reply = substr($r[0]['extid'],4); + else + return; + logger('facebook reply id=' . $reply); + } + + if($b['private'] && $reply === false) { + $allow_people = expand_acl($b['allow_cid']); + $allow_groups = expand_groups(expand_acl($b['allow_gid'])); + $deny_people = expand_acl($b['deny_cid']); + $deny_groups = expand_groups(expand_acl($b['deny_gid'])); + + $recipients = array_unique(array_merge($allow_people,$allow_groups)); + $deny = array_unique(array_merge($deny_people,$deny_groups)); + + $allow_str = dbesc(implode(', ',$recipients)); + if($allow_str) { + $r = q("SELECT `notify` FROM `contact` WHERE `id` IN ( $allow_str ) AND `network` = 'face'"); + $allow_arr = array(); + if(count($r)) + foreach($r as $rr) + $allow_arr[] = $rr['notify']; + } + + $deny_str = dbesc(implode(', ',$deny)); + if($deny_str) { + $r = q("SELECT `notify` FROM `contact` WHERE `id` IN ( $deny_str ) AND `network` = 'face'"); + $deny_arr = array(); + if(count($r)) + foreach($r as $rr) + $deny_arr[] = $rr['notify']; + } + + if(count($deny_arr) && (! count($allow_arr))) { + + // One or more FB folks were denied access but nobody on FB was specifically allowed access. + // This might cause the post to be open to public on Facebook, but only to selected members + // on another network. Since this could potentially leak a post to somebody who was denied, + // we will skip posting it to Facebook with a slightly vague but relevant message that will + // hopefully lead somebody to this code comment for a better explanation of what went wrong. + + notice( t('Post to Facebook cancelled because of multi-network access permission conflict.') . EOL); + return; + } + + + // if it's a private message but no Facebook members are allowed or denied, skip Facebook post + + if((! count($allow_arr)) && (! count($deny_arr))) + return; + } + + if($b['verb'] == ACTIVITY_LIKE) + $likes = true; + + + $appid = get_config('facebook', 'appid' ); + $secret = get_config('facebook', 'appsecret' ); + + if($appid && $secret) { + + logger('facebook: have appid+secret'); + + $fb_post = intval(get_pconfig(local_user(),'facebook','post')); + $fb_enable = (($fb_post && x($_POST,'facebook_enable')) ? intval($_POST['facebook_enable']) : 0); + $fb_token = get_pconfig(local_user(),'facebook','access_token'); + + // if API is used, default to the chosen settings + if($_POST['api_source'] && intval(get_pconfig(local_user(),'facebook','post_by_default'))) + $fb_enable = 1; + + + + + logger('facebook: $fb_post: ' . $fb_post . ' $fb_enable: ' . $fb_enable . ' $fb_token: ' . $fb_token,LOGGER_DEBUG); + + // post to facebook if it's a public post and we've ticked the 'post to Facebook' box, + // or it's a private message with facebook participants + // or it's a reply or likes action to an existing facebook post + + if($fb_post && $fb_token && ($fb_enable || $b['private'] || $reply)) { + logger('facebook: able to post'); + require_once('library/facebook.php'); + require_once('include/bbcode.php'); + + $msg = $b['body']; + + logger('Facebook post: original msg=' . $msg, LOGGER_DATA); + + // make links readable before we strip the code + + // unless it's a dislike - just send the text as a comment + + if($b['verb'] == ACTIVITY_DISLIKE) + $msg = trim(strip_tags(bbcode($msg))); + + $search_str = $a->get_baseurl() . '/search'; + + if(preg_match("/\[url=(.*?)\](.*?)\[\/url\]/is",$msg,$matches)) { + + // don't use hashtags for message link + + if(strpos($matches[2],$search_str) === false) { + $link = $matches[1]; + if(substr($matches[2],0,5) != '[img]') + $linkname = $matches[2]; + } + } + + $msg = preg_replace("/\[url=(.*?)\](.*?)\[\/url\]/is",'$2 $1',$msg); + + if(preg_match("/\[img\](.*?)\[\/img\]/is",$msg,$matches)) + $image = $matches[1]; + + $msg = preg_replace("/\[img\](.*?)\[\/img\]/is", t('Image: ') . '$1', $msg); + + if((strpos($link,z_root()) !== false) && (! $image)) + $image = $a->get_baseurl() . '/images/friendika-64.jpg'; + + $msg = trim(strip_tags(bbcode($msg))); + $msg = html_entity_decode($msg,ENT_QUOTES,'UTF-8'); + + // add any attachments as text urls + + $arr = explode(',',$b['attach']); + + if(count($arr)) { + $msg .= "\n"; + foreach($arr as $r) { + $matches = false; + $cnt = preg_match('|\[attach\]href=\"(.*?)\" size=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"\[\/attach\]|',$r,$matches); + if($cnt) { + $msg .= $matches[1]; + } + } + } + + if (strlen($msg) > FACEBOOK_MAXPOSTLEN) { + $shortlink = ""; + require_once('library/slinky.php'); + + $display_url = $a->get_baseurl() . '/display/' . $a->user['nickname'] . '/' . $b['id']; + $slinky = new Slinky( $display_url ); + // setup a cascade of shortening services + // try to get a short link from these services + // in the order ur1.ca, trim, id.gd, tinyurl + $slinky->set_cascade( array( new Slinky_UR1ca(), new Slinky_Trim(), new Slinky_IsGd(), new Slinky_TinyURL() ) ); + $shortlink = $slinky->short(); + // the new message will be shortened such that "... $shortlink" + // will fit into the character limit + $msg = substr($msg, 0, FACEBOOK_MAXPOSTLEN - strlen($shortlink) - 4); + $msg .= '... ' . $shortlink; + } + if(! strlen($msg)) + return; + + logger('Facebook post: msg=' . $msg, LOGGER_DATA); + + if($likes) { + $postvars = array('access_token' => $fb_token); + } + else { + $postvars = array( + 'access_token' => $fb_token, + 'message' => $msg + ); + if(isset($image)) + $postvars['picture'] = $image; + if(isset($link)) + $postvars['link'] = $link; + if(isset($linkname)) + $postvars['name'] = $linkname; + } + + if(($b['private']) && (! $b['parent'])) { + $postvars['privacy'] = '{"value": "CUSTOM", "friends": "SOME_FRIENDS"'; + if(count($allow_arr)) + $postvars['privacy'] .= ',"allow": "' . implode(',',$allow_arr) . '"'; + if(count($deny_arr)) + $postvars['privacy'] .= ',"deny": "' . implode(',',$deny_arr) . '"'; + $postvars['privacy'] .= '}'; + + } + + if($reply) { + $url = 'https://graph.facebook.com/' . $reply . '/' . (($likes) ? 'likes' : 'comments'); + } + else { + $url = 'https://graph.facebook.com/me/feed'; + if($b['plink']) + $postvars['actions'] = '{"name": "' . t('View on Friendika') . '", "link": "' . $b['plink'] . '"}'; + } + + logger('facebook: post to ' . $url); + logger('facebook: postvars: ' . print_r($postvars,true)); + + // "test_mode" prevents anything from actually being posted. + // Otherwise, let's do it. + + if(! get_config('facebook','test_mode')) { + $x = post_url($url, $postvars); + + $retj = json_decode($x); + if($retj->id) { + q("UPDATE `item` SET `extid` = '%s' WHERE `id` = %d LIMIT 1", + dbesc('fb::' . $retj->id), + intval($b['id']) + ); + } + else { + if(! $likes) { + $s = serialize(array('url' => $url, 'item' => $b['id'], 'post' => $postvars)); + q("INSERT INTO `queue` ( `network`, `cid`, `created`, `last`, `content`) + VALUES ( '%s', %d, '%s', '%s', '%s') ", + dbesc(NETWORK_FACEBOOK), + intval($a->contact), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc($s) + ); + + notice( t('Facebook post failed. Queued for retry.') . EOL); + } + } + + logger('Facebook post returns: ' . $x, LOGGER_DEBUG); + } + } + } + } +} + + +function fb_queue_hook(&$a,&$b) { + + $qi = q("SELECT * FROM `queue` WHERE `network` = '%s'", + dbesc(NETWORK_FACEBOOK) + ); + if(! count($qi)) + return; + + require_once('include/queue_fn.php'); + + foreach($qi as $x) { + if($x['network'] !== NETWORK_FACEBOOK) + continue; + + logger('facebook_queue: run'); + + $r = q("SELECT `user`.* FROM `user` LEFT JOIN `contact` on `contact`.`uid` = `user`.`uid` + WHERE `contact`.`self` = 1 AND `contact`.`id` = %d LIMIT 1", + intval($x['cid']) + ); + if(! count($r)) + continue; + + $user = $r[0]; + + $appid = get_config('facebook', 'appid' ); + $secret = get_config('facebook', 'appsecret' ); + + if($appid && $secret) { + $fb_post = intval(get_pconfig($user['uid'],'facebook','post')); + $fb_token = get_pconfig($user['uid'],'facebook','access_token'); + + if($fb_post && $fb_token) { + logger('facebook_queue: able to post'); + require_once('library/facebook.php'); + + $z = unserialize($x['content']); + $item = $z['item']; + $j = post_url($z['url'],$z['post']); + + $retj = json_decode($j); + if($retj->id) { + q("UPDATE `item` SET `extid` = '%s' WHERE `id` = %d LIMIT 1", + dbesc('fb::' . $retj->id), + intval($item) + ); + logger('facebook_queue: success: ' . $j); + remove_queue_item($x['id']); + } + else { + logger('facebook_queue: failed: ' . $j); + update_queue_time($x['id']); + } + } + } + } +} + +function fb_consume_all($uid) { + + require_once('include/items.php'); + + $access_token = get_pconfig($uid,'facebook','access_token'); + if(! $access_token) + return; + + if(! get_pconfig($uid,'facebook','no_wall')) { + $private_wall = intval(get_pconfig($uid,'facebook','private_wall')); + $s = fetch_url('https://graph.facebook.com/me/feed?access_token=' . $access_token); + if($s) { + $j = json_decode($s); + logger('fb_consume_stream: wall: ' . print_r($j,true), LOGGER_DATA); + fb_consume_stream($uid,$j,($private_wall) ? false : true); + } + } + $s = fetch_url('https://graph.facebook.com/me/home?access_token=' . $access_token); + if($s) { + $j = json_decode($s); + logger('fb_consume_stream: feed: ' . print_r($j,true), LOGGER_DATA); + fb_consume_stream($uid,$j,false); + } + +} + +function fb_consume_stream($uid,$j,$wall = false) { + + $a = get_app(); + + + $user = q("SELECT `nickname`, `blockwall` FROM `user` WHERE `uid` = %d AND `account_expired` = 0 LIMIT 1", + intval($uid) + ); + if(! count($user)) + return; + + $my_local_url = $a->get_baseurl() . '/profile/' . $user[0]['nickname']; + + $no_linking = get_pconfig($uid,'facebook','no_linking'); + if($no_linking) + return; + + $self = q("SELECT * FROM `contact` WHERE `self` = 1 AND `uid` = %d LIMIT 1", + intval($uid) + ); + + + $self_id = get_pconfig($uid,'facebook','self_id'); + if(! count($j->data) || (! strlen($self_id))) + return; + + foreach($j->data as $entry) { + logger('fb_consume: entry: ' . print_r($entry,true), LOGGER_DATA); + $datarray = array(); + + $r = q("SELECT * FROM `item` WHERE ( `uri` = '%s' OR `extid` = '%s') AND `uid` = %d LIMIT 1", + dbesc('fb::' . $entry->id), + dbesc('fb::' . $entry->id), + intval($uid) + ); + if(count($r)) { + $post_exists = true; + $orig_post = $r[0]; + $top_item = $r[0]['id']; + } + else { + $post_exists = false; + $orig_post = null; + } + + if(! $orig_post) { + $datarray['gravity'] = 0; + $datarray['uid'] = $uid; + $datarray['wall'] = (($wall) ? 1 : 0); + $datarray['uri'] = $datarray['parent-uri'] = 'fb::' . $entry->id; + $from = $entry->from; + if($from->id == $self_id) + $datarray['contact-id'] = $self[0]['id']; + else { + $r = q("SELECT * FROM `contact` WHERE `notify` = '%s' AND `uid` = %d AND `blocked` = 0 AND `readonly` = 0 LIMIT 1", + dbesc($from->id), + intval($uid) + ); + if(count($r)) + $datarray['contact-id'] = $r[0]['id']; + } + + // don't store post if we don't have a contact + + if(! x($datarray,'contact-id')) { + logger('no contact: post ignored'); + continue; + } + + $datarray['verb'] = ACTIVITY_POST; + if($wall) { + $datarray['owner-name'] = $self[0]['name']; + $datarray['owner-link'] = $self[0]['url']; + $datarray['owner-avatar'] = $self[0]['thumb']; + } + if(isset($entry->application) && isset($entry->application->name) && strlen($entry->application->name)) + $datarray['app'] = strip_tags($entry->application->name); + else + $datarray['app'] = 'facebook'; + $datarray['author-name'] = $from->name; + $datarray['author-link'] = 'http://facebook.com/profile.php?id=' . $from->id; + $datarray['author-avatar'] = 'https://graph.facebook.com/' . $from->id . '/picture'; + $datarray['plink'] = $datarray['author-link'] . '&v=wall&story_fbid=' . substr($entry->id,strpos($entry->id,'_') + 1); + + $datarray['body'] = $entry->message; + if($entry->picture) + $datarray['body'] .= "\n\n" . '[img]' . $entry->picture . '[/img]'; + if($entry->link) + $datarray['body'] .= "\n" . linkify($entry->link); + if($entry->name) + $datarray['body'] .= "\n" . $entry->name; + if($entry->caption) + $datarray['body'] .= "\n" . $entry->caption; + if($entry->description) + $datarray['body'] .= "\n" . $entry->description; + $datarray['created'] = datetime_convert('UTC','UTC',$entry->created_time); + $datarray['edited'] = datetime_convert('UTC','UTC',$entry->updated_time); + + // If the entry has a privacy policy, we cannot assume who can or cannot see it, + // as the identities are from a foreign system. Mark it as private to the owner. + + if($entry->privacy && $entry->privacy->value !== 'EVERYONE') { + $datarray['private'] = 1; + $datarray['allow_cid'] = '<' . $uid . '>'; + } + + $top_item = item_store($datarray); + $r = q("SELECT * FROM `item` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($top_item), + intval($uid) + ); + if(count($r)) { + $orig_post = $r[0]; + logger('fb: new top level item posted'); + } + } + + if(isset($entry->likes) && isset($entry->likes->data)) + $likers = $entry->likes->data; + else + $likers = null; + + if(isset($entry->comments) && isset($entry->comments->data)) + $comments = $entry->comments->data; + else + $comments = null; + + if(is_array($likers)) { + foreach($likers as $likes) { + + if(! $orig_post) + continue; + + // If we posted the like locally, it will be found with our url, not the FB url. + + $second_url = (($likes->id == $self_id) ? $self[0]['url'] : 'http://facebook.com/profile.php?id=' . $likes->id); + + $r = q("SELECT * FROM `item` WHERE `parent-uri` = '%s' AND `uid` = %d AND `verb` = '%s' + AND ( `author-link` = '%s' OR `author-link` = '%s' ) LIMIT 1", + dbesc($orig_post['uri']), + intval($uid), + dbesc(ACTIVITY_LIKE), + dbesc('http://facebook.com/profile.php?id=' . $likes->id), + dbesc($second_url) + ); + + if(count($r)) + continue; + + $likedata = array(); + $likedata['parent'] = $top_item; + $likedata['verb'] = ACTIVITY_LIKE; + $likedata['gravity'] = 3; + $likedata['uid'] = $uid; + $likedata['wall'] = (($wall) ? 1 : 0); + $likedata['uri'] = item_new_uri($a->get_baseurl(), $uid); + $likedata['parent-uri'] = $orig_post['uri']; + if($likes->id == $self_id) + $likedata['contact-id'] = $self[0]['id']; + else { + $r = q("SELECT * FROM `contact` WHERE `notify` = '%s' AND `uid` = %d AND `blocked` = 0 AND `readonly` = 0 LIMIT 1", + dbesc($likes->id), + intval($uid) + ); + if(count($r)) + $likedata['contact-id'] = $r[0]['id']; + } + if(! x($likedata,'contact-id')) + $likedata['contact-id'] = $orig_post['contact-id']; + + $likedata['app'] = 'facebook'; + $likedata['verb'] = ACTIVITY_LIKE; + $likedata['author-name'] = $likes->name; + $likedata['author-link'] = 'http://facebook.com/profile.php?id=' . $likes->id; + $likedata['author-avatar'] = 'https://graph.facebook.com/' . $likes->id . '/picture'; + + $author = '[url=' . $likedata['author-link'] . ']' . $likedata['author-name'] . '[/url]'; + $objauthor = '[url=' . $orig_post['author-link'] . ']' . $orig_post['author-name'] . '[/url]'; + $post_type = t('status'); + $plink = '[url=' . $orig_post['plink'] . ']' . $post_type . '[/url]'; + $likedata['object-type'] = ACTIVITY_OBJ_NOTE; + + $likedata['body'] = sprintf( t('%1$s likes %2$s\'s %3$s'), $author, $objauthor, $plink); + $likedata['object'] = '' . ACTIVITY_OBJ_NOTE . '1' . + '' . $orig_post['uri'] . '' . xmlify('') . '' . $orig_post['title'] . '' . $orig_post['body'] . ''; + + $item = item_store($likedata); + } + } + if(is_array($comments)) { + foreach($comments as $cmnt) { + + if(! $orig_post) + continue; + + $r = q("SELECT * FROM `item` WHERE `uid` = %d AND ( `uri` = '%s' OR `extid` = '%s' ) LIMIT 1", + intval($uid), + dbesc('fb::' . $cmnt->id), + dbesc('fb::' . $cmnt->id) + ); + if(count($r)) + continue; + + $cmntdata = array(); + $cmntdata['parent'] = $top_item; + $cmntdata['verb'] = ACTIVITY_POST; + $cmntdata['gravity'] = 6; + $cmntdata['uid'] = $uid; + $cmntdata['wall'] = (($wall) ? 1 : 0); + $cmntdata['uri'] = 'fb::' . $cmnt->id; + $cmntdata['parent-uri'] = $orig_post['uri']; + if($cmnt->from->id == $self_id) { + $cmntdata['contact-id'] = $self[0]['id']; + } + else { + $r = q("SELECT * FROM `contact` WHERE `notify` = '%s' AND `uid` = %d LIMIT 1", + dbesc($cmnt->from->id), + intval($uid) + ); + if(count($r)) { + $cmntdata['contact-id'] = $r[0]['id']; + if($r[0]['blocked'] || $r[0]['readonly']) + continue; + } + } + if(! x($cmntdata,'contact-id')) + $cmntdata['contact-id'] = $orig_post['contact-id']; + + $cmntdata['app'] = 'facebook'; + $cmntdata['created'] = datetime_convert('UTC','UTC',$cmnt->created_time); + $cmntdata['edited'] = datetime_convert('UTC','UTC',$cmnt->created_time); + $cmntdata['verb'] = ACTIVITY_POST; + $cmntdata['author-name'] = $cmnt->from->name; + $cmntdata['author-link'] = 'http://facebook.com/profile.php?id=' . $cmnt->from->id; + $cmntdata['author-avatar'] = 'https://graph.facebook.com/' . $cmnt->from->id . '/picture'; + $cmntdata['body'] = $cmnt->message; + $item = item_store($cmntdata); + } + } + } +} + diff --git a/fortunate.tgz b/fortunate.tgz new file mode 100644 index 00000000..5c9ce167 Binary files /dev/null and b/fortunate.tgz differ diff --git a/fortunate/fortunate.css b/fortunate/fortunate.css new file mode 100644 index 00000000..61813b7d --- /dev/null +++ b/fortunate/fortunate.css @@ -0,0 +1,7 @@ +.fortunate { + margin-top: 25px; + margin-left: 100px; + margin-bottom: 25px; + color: #000088; + font-size: 14px; +} \ No newline at end of file diff --git a/fortunate/fortunate.php b/fortunate/fortunate.php new file mode 100644 index 00000000..5a6302e5 --- /dev/null +++ b/fortunate/fortunate.php @@ -0,0 +1,27 @@ + + */ + + +function fortunate_install() { + register_hook('page_end', 'addon/fortunate/fortunate.php', 'fortunate_fetch'); +} + +function fortunate_uninstall() { + unregister_hook('page_end', 'addon/fortunate/fortunate.php', 'fortunate_fetch'); +} + + +function fortunate_fetch($a,&$b) { + + $a->page['htmlhead'] .= '' . "\r\n"; + + $s = fetch_url('http://fortunemod.com/cookie.php?numlines=2&equal=1&rand=' . mt_rand()); + $b .= '
' . $s . '
'; +} + diff --git a/impressum.tgz b/impressum.tgz new file mode 100644 index 00000000..901114e5 Binary files /dev/null and b/impressum.tgz differ diff --git a/impressum/README b/impressum/README new file mode 100644 index 00000000..8e4255bd --- /dev/null +++ b/impressum/README @@ -0,0 +1,27 @@ +Impressum Plugin for Friendika + +Author: Tobias Diekershoff + tobias.diekershoff@gmx.net + +License: 3-clause BSD license (same as Friendika) + +About + This plugin adds an Impressum block to the /friendika page with informations + about the page operator/owner and how to contact you in case of any questions. + + In the notes and postal fields you can use HTML tags for formatting. + +Configuration: + For configuration you can set the following variables in the .htconfig file + * $a->config['impressum']['owner'] this is the Name of the Operator + * $a->config['impressum']['ownerprofile'] this is an optional Friendika account + where the above owner name will link to + * $a->config['impressum']['email'] a contact email address (optional) + will be displayed slightly obfuscated + as name(at)example(dot)com + * $a->config['impressum']['postal'] should contain a postal address where + you can be reached at (optional) + * $a->config['impressum']['notes'] additional informations that should + be displayed in the Impressum block + + diff --git a/impressum/admin.tpl b/impressum/admin.tpl new file mode 100644 index 00000000..cfba8df7 --- /dev/null +++ b/impressum/admin.tpl @@ -0,0 +1,6 @@ +{{ inc field_input.tpl with $field=$owner }}{{ endinc }} +{{ inc field_input.tpl with $field=$ownerprofile }}{{ endinc }} +{{ inc field_input.tpl with $field=$postal }}{{ endinc }} +{{ inc field_input.tpl with $field=$notes }}{{ endinc }} +{{ inc field_input.tpl with $field=$email }}{{ endinc }} +
diff --git a/impressum/impressum.php b/impressum/impressum.php new file mode 100644 index 00000000..b760c7e0 --- /dev/null +++ b/impressum/impressum.php @@ -0,0 +1,76 @@ + + * License: 3-clause BSD license + */ + +function impressum_install() { + register_hook('about_hook', 'addon/impressum/impressum.php', 'impressum_show'); + logger("installed impressum plugin"); +} + +function impressum_uninstall() { + unregister_hook('about_hook', 'addon/impressum/impressum.php', 'impressum_show'); + logger("uninstalled impressum plugin"); +} +function obfuscate_email ($s) { + $s = str_replace('@','(at)',$s); + $s = str_replace('.','(dot)',$s); + return $s; +} +function impressum_show($a,&$b) { + $b .= '

'.t('Impressum').'

'; + $owner = get_config('impressum', 'owner'); + $owner_profile = get_config('impressum','ownerprofile'); + $postal = get_config('impressum', 'postal'); + $notes = get_config('impressum', 'notes'); + $email = obfuscate_email( get_config('impressum','email') ); + if (strlen($owner)) { + if (strlen($owner_profile)) { + $tmp = ''.$owner.''; + } else { + $tmp = $owner; + } + if (strlen($email)) { + $b .= '

'.t('Site Owner').': '. $tmp .'
'.t('Email Address').': '.$email.'

'; + } else { + $b .= '

'.t('Site Owner').': '. $tmp .'

'; + } + if (strlen($postal)) { + $b .= '

'.t('Postal Address').'
'. $postal .'

'; + } + if (strlen($notes)) { + $b .= '

'.$notes.'

'; + } + } else { + $b .= '

'.t('The impressum addon needs to be configured!
Please add at least the owner variable to your config file. For other variables please refer to the README file of the addon.').'

'; + } +} + +function impressum_plugin_admin_post (&$a) { + $owner = ((x($_POST, 'owner')) ? notags(trim($_POST['owner'])) : ''); + $ownerprofile = ((x($_POST, 'ownerprofile')) ? notags(trim($_POST['ownerprofile'])) : ''); + $postal = ((x($_POST, 'postal')) ? (trim($_POST['postal'])) : ''); + $notes = ((x($_POST, 'notes')) ? (trim($_POST['notes'])) : ''); + $email = ((x($_POST, 'email')) ? notags(trim($_POST['email'])) : ''); + set_config('impressum','owner',$owner); + set_config('impressum','ownerprofile',$ownerprofile); + set_config('impressum','postal',$postal); + set_config('impressum','email',$email); + set_config('impressum','notes',$notes); + info( t('Settings updated.'). EOL ); +} +function impressum_plugin_admin (&$a, &$o) { + $t = file_get_contents( dirname(__file__). "/admin.tpl" ); + $o = replace_macros($t, array( + '$submit' => t('Submit'), + '$owner' => array('owner', t('Site Owner'), get_config('impressum','owner'), ''), + '$ownerprofile' => array('ownerprofile', t('Site Owners Profile'), get_config('impressum','ownerprofile'), ''), + '$postal' => array('postal', t('Postal Address'), get_config('impressum','postal'), ''), + '$notes' => array('notes', t('Notes'), get_config('impressum','notes'), ''), + '$email' => array('email', t('Email Address'), get_config('impressum','email'), ''), + )); +} diff --git a/js_upload.tgz b/js_upload.tgz new file mode 100644 index 00000000..bfbbab6e Binary files /dev/null and b/js_upload.tgz differ diff --git a/js_upload/file-uploader/client/demo.htm b/js_upload/file-uploader/client/demo.htm new file mode 100644 index 00000000..2a0cd6d3 --- /dev/null +++ b/js_upload/file-uploader/client/demo.htm @@ -0,0 +1,38 @@ + + + + + + + + +

Back to project page

+ +

To upload a file, click on the button below. Drag-and-drop is supported in FF, Chrome.

+

Progress-bar is supported in FF3.6+, Chrome6+, Safari4+

+ +
+ +
+ + + + + \ No newline at end of file diff --git a/js_upload/file-uploader/client/do-nothing.htm b/js_upload/file-uploader/client/do-nothing.htm new file mode 100644 index 00000000..0da19059 --- /dev/null +++ b/js_upload/file-uploader/client/do-nothing.htm @@ -0,0 +1 @@ +{success:true} diff --git a/js_upload/file-uploader/client/fileuploader.css b/js_upload/file-uploader/client/fileuploader.css new file mode 100644 index 00000000..0e3f111a --- /dev/null +++ b/js_upload/file-uploader/client/fileuploader.css @@ -0,0 +1,31 @@ +.qq-uploader { position:relative; width: 100%;} + +.qq-upload-button { + display:block; /* or inline-block */ + width: 105px; padding: 7px 0; text-align:center; + background:#880000; border-bottom:1px solid #ddd;color:#fff; +} +.qq-upload-button-hover {background:#cc0000;} +.qq-upload-button-focus {outline:1px dotted black;} + +.qq-upload-drop-area { + position:absolute; top:0; left:0; width:100%; height:100%; min-height: 70px; z-index:2; + background:#FF9797; text-align:center; +} +.qq-upload-drop-area span { + display:block; position:absolute; top: 50%; width:100%; margin-top:-8px; font-size:16px; +} +.qq-upload-drop-area-active {background:#FF7171;} + +.qq-upload-list {margin:15px 35px; padding:0; list-style:disc;} +.qq-upload-list li { margin:0; padding:0; line-height:15px; font-size:12px;} +.qq-upload-file, .qq-upload-spinner, .qq-upload-size, .qq-upload-cancel, .qq-upload-failed-text { + margin-right: 7px; +} + +.qq-upload-file {} +.qq-upload-spinner {display:inline-block; background: url("loading.gif"); width:15px; height:15px; vertical-align:text-bottom;} +.qq-upload-size,.qq-upload-cancel {font-size:11px;} + +.qq-upload-failed-text {display:none;} +.qq-upload-fail .qq-upload-failed-text {display:inline;} \ No newline at end of file diff --git a/js_upload/file-uploader/client/fileuploader.js b/js_upload/file-uploader/client/fileuploader.js new file mode 100644 index 00000000..89c09ebf --- /dev/null +++ b/js_upload/file-uploader/client/fileuploader.js @@ -0,0 +1,1247 @@ +/** + * http://github.com/valums/file-uploader + * + * Multiple file upload component with progress-bar, drag-and-drop. + * © 2010 Andrew Valums ( andrew(at)valums.com ) + * + * Licensed under GNU GPL 2 or later, see license.txt. + */ + +// +// Helper functions +// + +var qq = qq || {}; + +/** + * Adds all missing properties from second obj to first obj + */ +qq.extend = function(first, second){ + for (var prop in second){ + first[prop] = second[prop]; + } +}; + +/** + * Searches for a given element in the array, returns -1 if it is not present. + * @param {Number} [from] The index at which to begin the search + */ +qq.indexOf = function(arr, elt, from){ + if (arr.indexOf) return arr.indexOf(elt, from); + + from = from || 0; + var len = arr.length; + + if (from < 0) from += len; + + for (; from < len; from++){ + if (from in arr && arr[from] === elt){ + return from; + } + } + return -1; +}; + +qq.getUniqueId = (function(){ + var id = 0; + return function(){ return id++; }; +})(); + +// +// Events + +qq.attach = function(element, type, fn){ + if (element.addEventListener){ + element.addEventListener(type, fn, false); + } else if (element.attachEvent){ + element.attachEvent('on' + type, fn); + } +}; +qq.detach = function(element, type, fn){ + if (element.removeEventListener){ + element.removeEventListener(type, fn, false); + } else if (element.attachEvent){ + element.detachEvent('on' + type, fn); + } +}; + +qq.preventDefault = function(e){ + if (e.preventDefault){ + e.preventDefault(); + } else{ + e.returnValue = false; + } +}; + +// +// Node manipulations + +/** + * Insert node a before node b. + */ +qq.insertBefore = function(a, b){ + b.parentNode.insertBefore(a, b); +}; +qq.remove = function(element){ + element.parentNode.removeChild(element); +}; + +qq.contains = function(parent, descendant){ + // compareposition returns false in this case + if (parent == descendant) return true; + + if (parent.contains){ + return parent.contains(descendant); + } else { + return !!(descendant.compareDocumentPosition(parent) & 8); + } +}; + +/** + * Creates and returns element from html string + * Uses innerHTML to create an element + */ +qq.toElement = (function(){ + var div = document.createElement('div'); + return function(html){ + div.innerHTML = html; + var element = div.firstChild; + div.removeChild(element); + return element; + }; +})(); + +// +// Node properties and attributes + +/** + * Sets styles for an element. + * Fixes opacity in IE6-8. + */ +qq.css = function(element, styles){ + if (styles.opacity != null){ + if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){ + styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')'; + } + } + qq.extend(element.style, styles); +}; +qq.hasClass = function(element, name){ + var re = new RegExp('(^| )' + name + '( |$)'); + return re.test(element.className); +}; +qq.addClass = function(element, name){ + if (!qq.hasClass(element, name)){ + element.className += ' ' + name; + } +}; +qq.removeClass = function(element, name){ + var re = new RegExp('(^| )' + name + '( |$)'); + element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, ""); +}; +qq.setText = function(element, text){ + element.innerText = text; + element.textContent = text; +}; + +// +// Selecting elements + +qq.children = function(element){ + var children = [], + child = element.firstChild; + + while (child){ + if (child.nodeType == 1){ + children.push(child); + } + child = child.nextSibling; + } + + return children; +}; + +qq.getByClass = function(element, className){ + if (element.querySelectorAll){ + return element.querySelectorAll('.' + className); + } + + var result = []; + var candidates = element.getElementsByTagName("*"); + var len = candidates.length; + + for (var i = 0; i < len; i++){ + if (qq.hasClass(candidates[i], className)){ + result.push(candidates[i]); + } + } + return result; +}; + +/** + * obj2url() takes a json-object as argument and generates + * a querystring. pretty much like jQuery.param() + * + * how to use: + * + * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');` + * + * will result in: + * + * `http://any.url/upload?otherParam=value&a=b&c=d` + * + * @param Object JSON-Object + * @param String current querystring-part + * @return String encoded querystring + */ +qq.obj2url = function(obj, temp, prefixDone){ + var uristrings = [], + prefix = '&', + add = function(nextObj, i){ + var nextTemp = temp + ? (/\[\]$/.test(temp)) // prevent double-encoding + ? temp + : temp+'['+i+']' + : i; + if ((nextTemp != 'undefined') && (i != 'undefined')) { + uristrings.push( + (typeof nextObj === 'object') + ? qq.obj2url(nextObj, nextTemp, true) + : (Object.prototype.toString.call(nextObj) === '[object Function]') + ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj()) + : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj) + ); + } + }; + + if (!prefixDone && temp) { + prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?'; + uristrings.push(temp); + uristrings.push(qq.obj2url(obj)); + } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) { + // we wont use a for-in-loop on an array (performance) + for (var i = 0, len = obj.length; i < len; ++i){ + add(obj[i], i); + } + } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){ + // for anything else but a scalar, we will use for-in-loop + for (var i in obj){ + add(obj[i], i); + } + } else { + uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj)); + } + + return uristrings.join(prefix) + .replace(/^&/, '') + .replace(/%20/g, '+'); +}; + +// +// +// Uploader Classes +// +// + +var qq = qq || {}; + +/** + * Creates upload button, validates upload, but doesn't create file list or dd. + */ +qq.FileUploaderBasic = function(o){ + this._options = { + // set to true to see the server response + debug: false, + action: '/server/upload', + params: {}, + button: null, + multiple: true, + maxConnections: 3, + // validation + allowedExtensions: [], + sizeLimit: 0, + minSizeLimit: 0, + // events + // return false to cancel submit + onSubmit: function(id, fileName){}, + onProgress: function(id, fileName, loaded, total){}, + onComplete: function(id, fileName, responseJSON){}, + onCancel: function(id, fileName){}, + // messages + messages: { + typeError: "{file} has invalid extension. Only {extensions} are allowed.", + sizeError: "{file} is too large, maximum file size is {sizeLimit}.", + minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", + emptyError: "{file} is empty, please select files again without it.", + onLeave: "The files are being uploaded, if you leave now the upload will be cancelled." + }, + showMessage: function(message){ + alert(message); + } + }; + qq.extend(this._options, o); + + // number of files being uploaded + this._filesInProgress = 0; + this._handler = this._createUploadHandler(); + + if (this._options.button){ + this._button = this._createUploadButton(this._options.button); + } + + this._preventLeaveInProgress(); +}; + +qq.FileUploaderBasic.prototype = { + setParams: function(params){ + this._options.params = params; + }, + getInProgress: function(){ + return this._filesInProgress; + }, + _createUploadButton: function(element){ + var self = this; + + return new qq.UploadButton({ + element: element, + multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(), + onChange: function(input){ + self._onInputChange(input); + } + }); + }, + _createUploadHandler: function(){ + var self = this, + handlerClass; + + if(qq.UploadHandlerXhr.isSupported()){ + handlerClass = 'UploadHandlerXhr'; + } else { + handlerClass = 'UploadHandlerForm'; + } + + var handler = new qq[handlerClass]({ + debug: this._options.debug, + action: this._options.action, + maxConnections: this._options.maxConnections, + onProgress: function(id, fileName, loaded, total){ + self._onProgress(id, fileName, loaded, total); + self._options.onProgress(id, fileName, loaded, total); + }, + onComplete: function(id, fileName, result){ + self._onComplete(id, fileName, result); + self._options.onComplete(id, fileName, result); + }, + onCancel: function(id, fileName){ + self._onCancel(id, fileName); + self._options.onCancel(id, fileName); + } + }); + + return handler; + }, + _preventLeaveInProgress: function(){ + var self = this; + + qq.attach(window, 'beforeunload', function(e){ + if (!self._filesInProgress){return;} + + var e = e || window.event; + // for ie, ff + e.returnValue = self._options.messages.onLeave; + // for webkit + return self._options.messages.onLeave; + }); + }, + _onSubmit: function(id, fileName){ + this._filesInProgress++; + }, + _onProgress: function(id, fileName, loaded, total){ + }, + _onComplete: function(id, fileName, result){ + this._filesInProgress--; + if (result.error){ + this._options.showMessage(result.error); + } + }, + _onCancel: function(id, fileName){ + this._filesInProgress--; + }, + _onInputChange: function(input){ + if (this._handler instanceof qq.UploadHandlerXhr){ + this._uploadFileList(input.files); + } else { + if (this._validateFile(input)){ + this._uploadFile(input); + } + } + this._button.reset(); + }, + _uploadFileList: function(files){ + for (var i=0; i this._options.sizeLimit){ + this._error('sizeError', name); + return false; + + } else if (size && size < this._options.minSizeLimit){ + this._error('minSizeError', name); + return false; + } + + return true; + }, + _error: function(code, fileName){ + var message = this._options.messages[code]; + function r(name, replacement){ message = message.replace(name, replacement); } + + r('{file}', this._formatFileName(fileName)); + r('{extensions}', this._options.allowedExtensions.join(', ')); + r('{sizeLimit}', this._formatSize(this._options.sizeLimit)); + r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit)); + + this._options.showMessage(message); + }, + _formatFileName: function(name){ + if (name.length > 33){ + name = name.slice(0, 19) + '...' + name.slice(-13); + } + return name; + }, + _isAllowedExtension: function(fileName){ + var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : ''; + var allowed = this._options.allowedExtensions; + + if (!allowed.length){return true;} + + for (var i=0; i 99); + + return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i]; + } +}; + + +/** + * Class that creates upload widget with drag-and-drop and file list + * @inherits qq.FileUploaderBasic + */ +qq.FileUploader = function(o){ + // call parent constructor + qq.FileUploaderBasic.apply(this, arguments); + + // additional options + qq.extend(this._options, { + element: null, + // if set, will be used instead of qq-upload-list in template + listElement: null, + + template: '
' + + '
Drop files here to upload
' + + '
Upload a file
' + + '
    ' + + '
    ', + + // template for one item in file list + fileTemplate: '
  • ' + + '' + + '' + + '' + + 'Cancel' + + 'Failed' + + '
  • ', + + classes: { + // used to get elements from templates + button: 'qq-upload-button', + drop: 'qq-upload-drop-area', + dropActive: 'qq-upload-drop-area-active', + list: 'qq-upload-list', + + file: 'qq-upload-file', + spinner: 'qq-upload-spinner', + size: 'qq-upload-size', + cancel: 'qq-upload-cancel', + + // added to list item when upload completes + // used in css to hide progress spinner + success: 'qq-upload-success', + fail: 'qq-upload-fail' + } + }); + // overwrite options with user supplied + qq.extend(this._options, o); + + this._element = this._options.element; + this._element.innerHTML = this._options.template; + this._listElement = this._options.listElement || this._find(this._element, 'list'); + + this._classes = this._options.classes; + + this._button = this._createUploadButton(this._find(this._element, 'button')); + + this._bindCancelEvent(); + this._setupDragDrop(); +}; + +// inherit from Basic Uploader +qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype); + +qq.extend(qq.FileUploader.prototype, { + /** + * Gets one of the elements listed in this._options.classes + **/ + _find: function(parent, type){ + var element = qq.getByClass(parent, this._options.classes[type])[0]; + if (!element){ + throw new Error('element not found ' + type); + } + + return element; + }, + _setupDragDrop: function(){ + var self = this, + dropArea = this._find(this._element, 'drop'); + + var dz = new qq.UploadDropZone({ + element: dropArea, + onEnter: function(e){ + qq.addClass(dropArea, self._classes.dropActive); + e.stopPropagation(); + }, + onLeave: function(e){ + e.stopPropagation(); + }, + onLeaveNotDescendants: function(e){ + qq.removeClass(dropArea, self._classes.dropActive); + }, + onDrop: function(e){ + dropArea.style.display = 'none'; + qq.removeClass(dropArea, self._classes.dropActive); + self._uploadFileList(e.dataTransfer.files); + } + }); + + dropArea.style.display = 'none'; + + qq.attach(document, 'dragenter', function(e){ + if (!dz._isValidFileDrag(e)) return; + + dropArea.style.display = 'block'; + }); + qq.attach(document, 'dragleave', function(e){ + if (!dz._isValidFileDrag(e)) return; + + var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); + // only fire when leaving document out + if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){ + dropArea.style.display = 'none'; + } + }); + }, + _onSubmit: function(id, fileName){ + qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments); + this._addToList(id, fileName); + }, + _onProgress: function(id, fileName, loaded, total){ + qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments); + + var item = this._getItemByFileId(id); + var size = this._find(item, 'size'); + size.style.display = 'inline'; + + var text; + if (loaded != total){ + text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total); + } else { + text = this._formatSize(total); + } + + qq.setText(size, text); + }, + _onComplete: function(id, fileName, result){ + qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments); + + // mark completed + var item = this._getItemByFileId(id); + qq.remove(this._find(item, 'cancel')); + qq.remove(this._find(item, 'spinner')); + + if (result.success){ + qq.addClass(item, this._classes.success); + } else { + qq.addClass(item, this._classes.fail); + } + }, + _addToList: function(id, fileName){ + var item = qq.toElement(this._options.fileTemplate); + item.qqFileId = id; + + var fileElement = this._find(item, 'file'); + qq.setText(fileElement, this._formatFileName(fileName)); + this._find(item, 'size').style.display = 'none'; + + this._listElement.appendChild(item); + }, + _getItemByFileId: function(id){ + var item = this._listElement.firstChild; + + // there can't be txt nodes in dynamically created list + // and we can use nextSibling + while (item){ + if (item.qqFileId == id) return item; + item = item.nextSibling; + } + }, + /** + * delegate click event for cancel link + **/ + _bindCancelEvent: function(){ + var self = this, + list = this._listElement; + + qq.attach(list, 'click', function(e){ + e = e || window.event; + var target = e.target || e.srcElement; + + if (qq.hasClass(target, self._classes.cancel)){ + qq.preventDefault(e); + + var item = target.parentNode; + self._handler.cancel(item.qqFileId); + qq.remove(item); + } + }); + } +}); + +qq.UploadDropZone = function(o){ + this._options = { + element: null, + onEnter: function(e){}, + onLeave: function(e){}, + // is not fired when leaving element by hovering descendants + onLeaveNotDescendants: function(e){}, + onDrop: function(e){} + }; + qq.extend(this._options, o); + + this._element = this._options.element; + + this._disableDropOutside(); + this._attachEvents(); +}; + +qq.UploadDropZone.prototype = { + _disableDropOutside: function(e){ + // run only once for all instances + if (!qq.UploadDropZone.dropOutsideDisabled ){ + + qq.attach(document, 'dragover', function(e){ + if (e.dataTransfer){ + e.dataTransfer.dropEffect = 'none'; + e.preventDefault(); + } + }); + + qq.UploadDropZone.dropOutsideDisabled = true; + } + }, + _attachEvents: function(){ + var self = this; + + qq.attach(self._element, 'dragover', function(e){ + if (!self._isValidFileDrag(e)) return; + + var effect = e.dataTransfer.effectAllowed; + if (effect == 'move' || effect == 'linkMove'){ + e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed) + } else { + e.dataTransfer.dropEffect = 'copy'; // for Chrome + } + + e.stopPropagation(); + e.preventDefault(); + }); + + qq.attach(self._element, 'dragenter', function(e){ + if (!self._isValidFileDrag(e)) return; + + self._options.onEnter(e); + }); + + qq.attach(self._element, 'dragleave', function(e){ + if (!self._isValidFileDrag(e)) return; + + self._options.onLeave(e); + + var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); + // do not fire when moving a mouse over a descendant + if (qq.contains(this, relatedTarget)) return; + + self._options.onLeaveNotDescendants(e); + }); + + qq.attach(self._element, 'drop', function(e){ + if (!self._isValidFileDrag(e)) return; + + e.preventDefault(); + self._options.onDrop(e); + }); + }, + _isValidFileDrag: function(e){ + var dt = e.dataTransfer, + // do not check dt.types.contains in webkit, because it crashes safari 4 + isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1; + + // dt.effectAllowed is none in Safari 5 + // dt.types.contains check is for firefox + return dt && dt.effectAllowed != 'none' && + (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files'))); + + } +}; + +qq.UploadButton = function(o){ + this._options = { + element: null, + // if set to true adds multiple attribute to file input + multiple: false, + // name attribute of file input + name: 'file', + onChange: function(input){}, + hoverClass: 'qq-upload-button-hover', + focusClass: 'qq-upload-button-focus' + }; + + qq.extend(this._options, o); + + this._element = this._options.element; + + // make button suitable container for input + qq.css(this._element, { + position: 'relative', + overflow: 'hidden', + // Make sure browse button is in the right side + // in Internet Explorer + direction: 'ltr' + }); + + this._input = this._createInput(); +}; + +qq.UploadButton.prototype = { + /* returns file input element */ + getInput: function(){ + return this._input; + }, + /* cleans/recreates the file input */ + reset: function(){ + if (this._input.parentNode){ + qq.remove(this._input); + } + + qq.removeClass(this._element, this._options.focusClass); + this._input = this._createInput(); + }, + _createInput: function(){ + var input = document.createElement("input"); + + if (this._options.multiple){ + input.setAttribute("multiple", "multiple"); + } + + input.setAttribute("type", "file"); + input.setAttribute("name", this._options.name); + + qq.css(input, { + position: 'absolute', + // in Opera only 'browse' button + // is clickable and it is located at + // the right side of the input + right: 0, + top: 0, + fontFamily: 'Arial', + // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118 + fontSize: '118px', + margin: 0, + padding: 0, + cursor: 'pointer', + opacity: 0 + }); + + this._element.appendChild(input); + + var self = this; + qq.attach(input, 'change', function(){ + self._options.onChange(input); + }); + + qq.attach(input, 'mouseover', function(){ + qq.addClass(self._element, self._options.hoverClass); + }); + qq.attach(input, 'mouseout', function(){ + qq.removeClass(self._element, self._options.hoverClass); + }); + qq.attach(input, 'focus', function(){ + qq.addClass(self._element, self._options.focusClass); + }); + qq.attach(input, 'blur', function(){ + qq.removeClass(self._element, self._options.focusClass); + }); + + // IE and Opera, unfortunately have 2 tab stops on file input + // which is unacceptable in our case, disable keyboard access + if (window.attachEvent){ + // it is IE or Opera + input.setAttribute('tabIndex', "-1"); + } + + return input; + } +}; + +/** + * Class for uploading files, uploading itself is handled by child classes + */ +qq.UploadHandlerAbstract = function(o){ + this._options = { + debug: false, + action: '/upload.php', + // maximum number of concurrent uploads + maxConnections: 999, + onProgress: function(id, fileName, loaded, total){}, + onComplete: function(id, fileName, response){}, + onCancel: function(id, fileName){} + }; + qq.extend(this._options, o); + + this._queue = []; + // params for files in queue + this._params = []; +}; +qq.UploadHandlerAbstract.prototype = { + log: function(str){ + if (this._options.debug && window.console) console.log('[uploader] ' + str); + }, + /** + * Adds file or file input to the queue + * @returns id + **/ + add: function(file){}, + /** + * Sends the file identified by id and additional query params to the server + */ + upload: function(id, params){ + var len = this._queue.push(id); + + var copy = {}; + qq.extend(copy, params); + this._params[id] = copy; + + // if too many active uploads, wait... + if (len <= this._options.maxConnections){ + this._upload(id, this._params[id]); + } + }, + /** + * Cancels file upload by id + */ + cancel: function(id){ + this._cancel(id); + this._dequeue(id); + }, + /** + * Cancells all uploads + */ + cancelAll: function(){ + for (var i=0; i= max){ + var nextId = this._queue[max-1]; + this._upload(nextId, this._params[nextId]); + } + } +}; + +/** + * Class for uploading files using form and iframe + * @inherits qq.UploadHandlerAbstract + */ +qq.UploadHandlerForm = function(o){ + qq.UploadHandlerAbstract.apply(this, arguments); + + this._inputs = {}; +}; +// @inherits qq.UploadHandlerAbstract +qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype); + +qq.extend(qq.UploadHandlerForm.prototype, { + add: function(fileInput){ + fileInput.setAttribute('name', 'qqfile'); + var id = 'qq-upload-handler-iframe' + qq.getUniqueId(); + + this._inputs[id] = fileInput; + + // remove file input from DOM + if (fileInput.parentNode){ + qq.remove(fileInput); + } + + return id; + }, + getName: function(id){ + // get input value and remove path to normalize + return this._inputs[id].value.replace(/.*(\/|\\)/, ""); + }, + _cancel: function(id){ + this._options.onCancel(id, this.getName(id)); + + delete this._inputs[id]; + + var iframe = document.getElementById(id); + if (iframe){ + // to cancel request set src to something else + // we use src="javascript:false;" because it doesn't + // trigger ie6 prompt on https + iframe.setAttribute('src', 'javascript:false;'); + + qq.remove(iframe); + } + }, + _upload: function(id, params){ + var input = this._inputs[id]; + + if (!input){ + throw new Error('file with passed id was not added, or already uploaded or cancelled'); + } + + var fileName = this.getName(id); + + var iframe = this._createIframe(id); + var form = this._createForm(iframe, params); + form.appendChild(input); + + var self = this; + this._attachLoadEvent(iframe, function(){ + self.log('iframe loaded'); + + var response = self._getIframeContentJSON(iframe); + + self._options.onComplete(id, fileName, response); + self._dequeue(id); + + delete self._inputs[id]; + // timeout added to fix busy state in FF3.6 + setTimeout(function(){ + qq.remove(iframe); + }, 1); + }); + + form.submit(); + qq.remove(form); + + return id; + }, + _attachLoadEvent: function(iframe, callback){ + qq.attach(iframe, 'load', function(){ + // when we remove iframe from dom + // the request stops, but in IE load + // event fires + if (!iframe.parentNode){ + return; + } + + // fixing Opera 10.53 + if (iframe.contentDocument && + iframe.contentDocument.body && + iframe.contentDocument.body.innerHTML == "false"){ + // In Opera event is fired second time + // when body.innerHTML changed from false + // to server response approx. after 1 sec + // when we upload file with iframe + return; + } + + callback(); + }); + }, + /** + * Returns json object received by iframe from server. + */ + _getIframeContentJSON: function(iframe){ + // iframe.contentWindow.document - for IE<7 + var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document, + response; + + this.log("converting iframe's innerHTML to JSON"); + this.log("innerHTML = " + doc.body.innerHTML); + + try { + response = eval("(" + doc.body.innerHTML + ")"); + } catch(err){ + response = {}; + } + + return response; + }, + /** + * Creates iframe with unique name + */ + _createIframe: function(id){ + // We can't use following code as the name attribute + // won't be properly registered in IE6, and new window + // on form submit will open + // var iframe = document.createElement('iframe'); + // iframe.setAttribute('name', id); + + var iframe = qq.toElement('