diff options
79 files changed, 1567 insertions, 269 deletions
@@ -3,26 +3,26 @@ Hubzilla - Community Server =========================== -Help us redefine the web - using integrated and united community websites. --------------------------------------------------------------------------- +Connected and linked web communities. +------------------------------------- <p align="center" markdown="1"> <em><a href="https://github.com/redmatrix/hubzilla/blob/master/install/INSTALL.txt">Installing Hubzilla</a></em> </p> -**What are Hubs?** +**What are Hubz?** -Hubs are independent general-purpose websites that not only connect with their associated members and viewers, but also connect together to exchange personal communications and other information with each other. +Hubz are independent general-purpose websites that not only connect with their associated members and viewers, but also connect together to exchange personal communications and other information with each other. This allows hub members on any hub to securely and privately share anything; with anybody, on any hub - anywhere; or share stuff publicly with anybody on the internet if desired. -**Hubzilla** is the server software which makes this possible. It is a sophisticated and unique combination of an open source content management system and a decentralised identity, communications, and permissions framework and protocol suite, built using common webserver technology (PHP/MySQL/Apache, although Mariadb or Postgres and Nginx could also be used - we're pretty easy). The end result is a level of systems integration, privacy control, and communications features that you wouldn't think are possible in either a content management system or a decentralised communications network. It also brings a new level of cooperation and privacy to the web and introduces the concept of personally owned "single sign-on" to web services across the entire internet. +**Hubzilla** is the server software which makes this possible. It is a sophisticated and unique combination of an open source content management system and a decentralised identity, communications, and permissions framework and protocol suite, built using common webserver technology (PHP/MySQL/Apache and popular variants). The end result is a level of systems integration, privacy control, and communications features that you wouldn't think are possible in either a content management system or a decentralised communications network. It also brings a new level of cooperation and privacy to the web and introduces the concept of personally owned "single sign-on" to web services across the entire internet. -Hubzilla hubs are +Hubzilla hubz are * decentralised * inherently social -* optionally inter-networked with other hubs -* privacy-enabled (privacy exclusions work across the entire internet to any registered identity on any compatible hubs) +* optionally inter-networked with other hubz +* privacy-enabled (privacy exclusions work across the entire internet to any registered identity on any compatible hubz) Possible website applications include diff --git a/Zotlabs/Project/System.php b/Zotlabs/Project/System.php index f87f827bb..a67742db5 100644 --- a/Zotlabs/Project/System.php +++ b/Zotlabs/Project/System.php @@ -5,21 +5,24 @@ namespace Zotlabs\Project; class System { function get_platform_name() { - $a = get_app(); if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['platform_name']) return \App::$config['system']['platform_name']; return PLATFORM_NAME; } + function get_site_name() { + if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['sitename']) + return \App::$config['system']['sitename']; + return ''; + } + function get_project_version() { - $a = get_app(); if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['hide_version']) return ''; return RED_VERSION; } function get_update_version() { - $a = get_app(); if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['hide_version']) return ''; return DB_UPDATE_VERSION; @@ -27,14 +30,12 @@ class System { function get_notify_icon() { - $a = get_app(); if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['email_notify_icon_url']) return \App::$config['system']['email_notify_icon_url']; return z_root() . '/images/hz-white-32.png'; } function get_site_icon() { - $a = get_app(); if(is_array(\App::$config) && is_array(\App::$config['system']) && \App::$config['system']['site_icon_url']) return \App::$config['system']['site_icon_url']; return z_root() . '/images/hz-32.png'; diff --git a/Zotlabs/Storage/Directory.php b/Zotlabs/Storage/Directory.php index 0347ce087..edbef5a95 100644 --- a/Zotlabs/Storage/Directory.php +++ b/Zotlabs/Storage/Directory.php @@ -168,6 +168,14 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota { intval($this->auth->owner_id) ); + + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->folder_hash); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } + $this->red_path = $new_path; } @@ -335,6 +343,12 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota { $p = photo_upload($c[0],\App::get_observer(),$args); } + $sync = attach_export_data($c[0],$hash); + + if($sync) + build_sync_packet($c[0]['channel_id'],array('file' => array($sync))); + + } /** @@ -356,7 +370,14 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota { if ($r) { $result = attach_mkdir($r[0], $this->auth->observer, array('filename' => $name, 'folder' => $this->folder_hash)); - if (! $result['success']) { + + if($result['success']) { + $sync = attach_export_data($r[0],$ret['data']['hash']); + if($sync) { + build_sync_packet($r[0]['channel_id'],array('file' => array($sync))); + } + } + else { logger('error ' . print_r($result, true), LOGGER_DEBUG); } } @@ -380,6 +401,15 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota { } attach_delete($this->auth->owner_id, $this->folder_hash); + + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->folder_hash,true); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } + + } diff --git a/Zotlabs/Storage/File.php b/Zotlabs/Storage/File.php index a4bf3f49d..897f24edd 100644 --- a/Zotlabs/Storage/File.php +++ b/Zotlabs/Storage/File.php @@ -84,6 +84,20 @@ class File extends DAV\Node implements DAV\IFile { dbesc($this->data['hash']), intval($this->data['id']) ); + + if($this->data->is_photo) { + $r = q("update photo set filename = '%s' where resource_id = '%s' and uid = %d", + dbesc($newName), + dbesc($this->data['hash']), + intval($this->auth->owner_id) + ); + } + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->data['hash']); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } } /** @@ -205,6 +219,12 @@ class File extends DAV\Node implements DAV\IFile { return; } } + + $sync = attach_export_data($c[0],$this->data['hash']); + + if($sync) + build_sync_packet($c[0]['channel_id'],array('file' => array($sync))); + } /** @@ -318,5 +338,12 @@ class File extends DAV\Node implements DAV\IFile { } attach_delete($this->auth->owner_id, $this->data['hash']); + + $ch = channelx_by_n($this->auth->owner_id); + if($ch) { + $sync = attach_export_data($ch,$this->data['hash'],true); + if($sync) + build_sync_packet($ch['channel_id'],array('file' => array($sync))); + } } } diff --git a/Zotlabs/Web/Session.php b/Zotlabs/Web/Session.php new file mode 100644 index 000000000..55536fdc7 --- /dev/null +++ b/Zotlabs/Web/Session.php @@ -0,0 +1,160 @@ +<?php + +namespace Zotlabs\Web; + +/** + * + * @brief This file includes session related functions. + * + * Session management functions. These provide database storage of PHP + * session info. + */ + + +class Session { + + private static $handler = null; + private static $session_started = false; + + function init() { + + $gc_probability = 50; + + ini_set('session.gc_probability', $gc_probability); + ini_set('session.use_only_cookies', 1); + ini_set('session.cookie_httponly', 1); + + /* + * Set our session storage functions. + */ + + $handler = new \Zotlabs\Web\SessionHandler(); + self::$handler = $handler; + + $x = session_set_save_handler($handler,true); + if(! $x) + logger('Session save handler initialisation failed.',LOGGER_NORMAL,LOG_ERR); + + // Force cookies to be secure (https only) if this site is SSL enabled. + // Must be done before session_start(). + + $arr = session_get_cookie_params(); + session_set_cookie_params( + ((isset($arr['lifetime'])) ? $arr['lifetime'] : 0), + ((isset($arr['path'])) ? $arr['path'] : '/'), + ((isset($arr['domain'])) ? $arr['domain'] : App::get_hostname()), + ((isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on') ? true : false), + ((isset($arr['httponly'])) ? $arr['httponly'] : true) + ); + } + + function start() { + session_start(); + self::$session_started = true; + } + + /** + * @brief Resets the current session. + * + * @return void + */ + + function nuke() { + self::new_cookie(0); // 0 means delete on browser exit + if($_SESSION && count($_SESSION)) { + foreach($_SESSION as $k => $v) { + unset($_SESSION[$k]); + } + } + } + + function new_cookie($xtime) { + + $newxtime = (($xtime> 0) ? (time() + $xtime) : 0); + + $old_sid = session_id(); + + if(self::$handler && self::$session_started) { + session_regenerate_id(true); + + // force SessionHandler record creation with the new session_id + // which occurs as a side effect of read() + + self::$handler->read(session_id()); + } + else + logger('no session handler'); + + if (x($_COOKIE, 'jsAvailable')) { + setcookie('jsAvailable', $_COOKIE['jsAvailable'], $newxtime); + } + setcookie(session_name(),session_id(),$newxtime); + + $arr = array('expire' => $xtime); + call_hooks('new_cookie', $arr); + + } + + function extend_cookie() { + + // if there's a long-term cookie, extend it + + $xtime = (($_SESSION['remember_me']) ? (60 * 60 * 24 * 365) : 0 ); + + if($xtime) + setcookie(session_name(),session_id(),(time() + $xtime)); + $arr = array('expire' => $xtime); + call_hooks('extend_cookie', $arr); + + } + + + function return_check() { + + // check a returning visitor against IP changes. + // If the change results in being blocked from re-entry with the current cookie + // nuke the session and logout. + // Returning at all indicates the session is still valid. + + // first check if we're enforcing that sessions can't change IP address + // @todo what to do with IPv6 addresses + + if($_SESSION['addr'] && $_SESSION['addr'] != $_SERVER['REMOTE_ADDR']) { + logger('SECURITY: Session IP address changed: ' . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); + + $partial1 = substr($_SESSION['addr'], 0, strrpos($_SESSION['addr'], '.')); + $partial2 = substr($_SERVER['REMOTE_ADDR'], 0, strrpos($_SERVER['REMOTE_ADDR'], '.')); + + $paranoia = intval(get_pconfig($_SESSION['uid'], 'system', 'paranoia')); + + if(! $paranoia) + $paranoia = intval(get_config('system', 'paranoia')); + + switch($paranoia) { + case 0: + // no IP checking + break; + case 2: + // check 2 octets + $partial1 = substr($partial1, 0, strrpos($partial1, '.')); + $partial2 = substr($partial2, 0, strrpos($partial2, '.')); + if($partial1 == $partial2) + break; + case 1: + // check 3 octets + if($partial1 == $partial2) + break; + case 3: + default: + // check any difference at all + logger('Session address changed. Paranoid setting in effect, blocking session. ' + . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); + self::nuke(); + goaway(z_root()); + break; + } + } + return true; + } + +} diff --git a/Zotlabs/Web/SessionHandler.php b/Zotlabs/Web/SessionHandler.php new file mode 100644 index 000000000..6980a6408 --- /dev/null +++ b/Zotlabs/Web/SessionHandler.php @@ -0,0 +1,88 @@ +<?php + +namespace Zotlabs\Web; + + +class SessionHandler implements \SessionHandlerInterface { + + + function open ($s, $n) { + return true; + } + + // IMPORTANT: if we read the session and it doesn't exist, create an empty record. + // We rely on this due to differing PHP implementation of session_regenerate_id() + // some which call read explicitly and some that do not. So we call it explicitly + // just after sid regeneration to force a record to exist. + + function read ($id) { + + if($id) { + $r = q("SELECT `data` FROM `session` WHERE `sid`= '%s'", dbesc($id)); + + if($r) { + return $r[0]['data']; + } + else { + q("INSERT INTO `session` (sid, expire) values ('%s', '%s')", + dbesc($id), + dbesc(time() + 300) + ); + } + } + + return ''; + } + + + function write ($id, $data) { + + if(! $id || ! $data) { + return false; + } + + // Unless we authenticate somehow, only keep a session for 5 minutes + // The viewer can extend this by performing any web action using the + // original cookie, but this allows us to cleanup the hundreds or + // thousands of empty sessions left around from web crawlers which are + // assigned cookies on each page that they never use. + + $expire = time() + 300; + + if($_SESSION) { + if(array_key_exists('remember_me',$_SESSION) && intval($_SESSION['remember_me'])) + $expire = time() + (60 * 60 * 24 * 365); + elseif(local_channel()) + $expire = time() + (60 * 60 * 24 * 3); + elseif(remote_channel()) + $expire = time() + (60 * 60 * 24 * 1); + } + + q("UPDATE `session` + SET `data` = '%s', `expire` = '%s' WHERE `sid` = '%s'", + dbesc($data), + dbesc($expire), + dbesc($id) + ); + + return true; + } + + + function close() { + return true; + } + + + function destroy ($id) { + q("DELETE FROM `session` WHERE `sid` = '%s'", dbesc($id)); + return true; + } + + + function gc($expire) { + q("DELETE FROM session WHERE expire < %d", dbesc(time())); + return true; + } + +} @@ -454,7 +454,7 @@ define ( 'TERM_OBJ_APP', 7 ); /** * various namespaces we may need to parse */ - +define ( 'PROTOCOL_ZOT', 'http://purl.org/zot/protocol' ); define ( 'NAMESPACE_ZOT', 'http://purl.org/zot' ); define ( 'NAMESPACE_DFRN' , 'http://purl.org/macgirvin/dfrn/1.0' ); define ( 'NAMESPACE_THREAD' , 'http://purl.org/syndication/thread/1.0' ); @@ -1069,6 +1069,7 @@ class App { '$local_channel' => local_channel(), '$metas' => self::$meta->get(), '$update_interval' => $interval, + 'osearch' => sprintf( t('Search %1$s (%2$s)','opensearch'), Zotlabs\Project\System::get_site_name(), t('$Projectname','opensearch')), '$icon' => head_get_icon(), '$head_css' => head_get_css(), '$head_js' => head_get_js(), @@ -1542,6 +1543,24 @@ function fix_system_urls($oldurl, $newurl) { proc_run('php', 'include/notifier.php', 'refresh_all', $c[0]['channel_id']); } } + + // now replace any remote xchans whose photos are stored locally (which will be most if not all remote xchans) + + $r = q("select * from xchan where xchan_photo_l like '%s'", + dbesc($oldurl . '%') + ); + + if($r) { + foreach($r as $rr) { + $x = q("update xchan set xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s' where xchan_hash = '%s'", + dbesc(str_replace($oldurl,$newurl,$rr['xchan_photo_l'])), + dbesc(str_replace($oldurl,$newurl,$rr['xchan_photo_m'])), + dbesc(str_replace($oldurl,$newurl,$rr['xchan_photo_s'])), + dbesc($rr['xchan_hash']) + ); + } + } + } @@ -1581,7 +1600,7 @@ function login($register = false, $form_id = 'main-login', $hiddens=false) { '$form_id' => $form_id, '$lname' => array('username', t('Email') , '', ''), '$lpassword' => array('password', t('Password'), '', ''), - '$remember' => array('remember', t('Remember me'), '', '',array(t('No'),t('Yes'))), + '$remember_me' => array('remember_me', t('Remember me'), '', '',array(t('No'),t('Yes'))), '$hiddens' => $hiddens, '$register' => $reg, '$lostpass' => t('Forgot your password?'), @@ -1598,7 +1617,6 @@ function login($register = false, $form_id = 'main-login', $hiddens=false) { * @brief Used to end the current process, after saving session state. */ function killme() { - session_write_close(); exit; } diff --git a/doc/context/admin/security/help.html b/doc/context/admin/security/help.html new file mode 100644 index 000000000..e9a741a5e --- /dev/null +++ b/doc/context/admin/security/help.html @@ -0,0 +1,3 @@ +<h3>Security Settings</h3> +<p>This page contains various administrator settings related to security.</p> +<p>To save any changes you make to these settings, you must press the Submit button.</p>
\ No newline at end of file diff --git a/doc/context/channel/help.html b/doc/context/channel/help.html new file mode 100644 index 000000000..810913ff3 --- /dev/null +++ b/doc/context/channel/help.html @@ -0,0 +1,24 @@ +<script> + var contextualHelp1 = function (target, openSidePanel) { + $("#help-content").removeClass('help-content-open'); // Close the help panel + $("#navbar-collapse-1").removeClass('in'); // Collapse the navbar for small screens + if (openSidePanel) { + $("main").addClass('region_1-on'); // Open the side panel to highlight element + } else { + $("main").removeClass('region_1-on'); + } + // Animate the page scroll to the element and then pulse the element to direct attention + $('html,body').animate({scrollTop: $(target).offset().top - $('#navbar-collapse-1').height() - 20}, 'slow'); + for (i = 0; i < 3; i++) { + $(target).fadeTo('slow', 0.1).fadeTo('slow', 1.0); + } + } +</script> +<dl class="dl-horizontal"> + <dt>General</dt> + <dd>This is the home page of a channel. It is similar to someone's profile "wall" in a social network context. Posts created by the channel are displayed according to the observer's viewing permissions.</dd> + <dt>Create a Post</dt> + <dd>If you have permission to create posts on the channel page, then you will see the post editor at the top.</dd> + <dt><a href='#' onclick='contextualHelp1("#tabs-collapse-1", 0); return false;' title="Click to highlight element...">Channel Content Tabs</a></dt> + <dd>The channel content tabs are links to other content published by the channel. The <b>About</b> tab links to the channel profile. The <b>Photos</b> tab links to the channel photo galleries. The <b>Files</b> tab links to the general shared files published by the channel.</dd> +</dl>
\ No newline at end of file diff --git a/doc/context/cloud/help.html b/doc/context/cloud/help.html new file mode 100644 index 000000000..105947517 --- /dev/null +++ b/doc/context/cloud/help.html @@ -0,0 +1,22 @@ +<script> + var contextualHelp1 = function (target, openSidePanel) { + $("#help-content").removeClass('help-content-open'); // Close the help panel + $("#navbar-collapse-1").removeClass('in'); // Collapse the navbar for small screens + if (openSidePanel) { + $("main").addClass('region_1-on'); // Open the side panel to highlight element + } else { + $("main").removeClass('region_1-on'); + } + // Animate the page scroll to the element and then pulse the element to direct attention + $('html,body').animate({scrollTop: $(target).offset().top - $('#navbar-collapse-1').height() - 20}, 'slow'); + for (i = 0; i < 3; i++) { + $(target).fadeTo('slow', 0.1).fadeTo('slow', 1.0); + } + } +</script> +<dl class="dl-horizontal"> + <dt>General</dt> + <dd>This page displays a channel's "cloud" files. The files visible to the observer depend on the individual file permissions set by the channel owner. If you have permission to create/upload files you will see control buttons above the file list.</dd> + <dt><a href='#' onclick='contextualHelp1("#tabs-collapse-1", 0); return false;' title="Click to highlight element...">Channel Content Tabs</a></dt> + <dd>The channel content tabs are links to other content published by the channel. The <b>About</b> tab links to the channel profile. The <b>Photos</b> tab links to the channel photo galleries. The <b>Files</b> tab links to the general shared files published by the channel.</dd> +</dl>
\ No newline at end of file diff --git a/doc/context/mail/help.html b/doc/context/mail/help.html new file mode 100644 index 000000000..a2361a135 --- /dev/null +++ b/doc/context/mail/help.html @@ -0,0 +1,10 @@ +<dl class="dl-horizontal"> + <dt>General</dt> + <dd>The messages displayed in private mail are visible only to you and the single recipient. </dd> + <dt>Combined View</dt> + <dd>Complete conversations can be viewed in a continuous thread by selecting <b>Combined View</b>. Available conversations are displayed beneath the menu in the side panel.</dd> + <dt>Inbox/Outbox</dt> + <dd>Individual sent messages are viewed by selecting <b>Outbox</b>, and incoming messages are viewed using the <b>Inbox</b> filter.</dd> + <dt>New Message</dt> + <dd>Individual messages have delivery reports that can be viewed using the drop-down menu. Messages can also be recalled from the same menu, which can prevent the recipient from viewing the message <i>if they have not already read it</i>.</dd> +</dl>
\ No newline at end of file diff --git a/doc/context/network/help.html b/doc/context/network/help.html new file mode 100644 index 000000000..956af7380 --- /dev/null +++ b/doc/context/network/help.html @@ -0,0 +1,26 @@ +<script> + var contextualHelp1 = function (target, openSidePanel) { + $("#help-content").removeClass('help-content-open'); // Close the help panel + $("#navbar-collapse-1").removeClass('in'); // Collapse the navbar for small screens + if (openSidePanel) { + $("main").addClass('region_1-on'); // Open the side panel to highlight element + } else { + $("main").removeClass('region_1-on'); + } + // Animate the page scroll to the element and then pulse the element to direct attention + $('html,body').animate({scrollTop: $(target).offset().top - $('#navbar-collapse-1').height() - 20}, 'slow'); + for (i = 0; i < 3; i++) { + $(target).fadeTo('slow', 0.1).fadeTo('slow', 1.0); + } + } +</script> +<dl class="dl-horizontal"> + <dt>General</dt> + <dd>The network page displays a stream of posts and conversations, typically ordered by the most recently updated. This page is highly customizable.</dd> + <dt><a href='#' onclick='contextualHelp1("#profile-jot-wrapper", 0); return false;' title="Click to highlight element...">Create a Post</a></dt> + <dd>At the top of the page there is a text box that says "Share". Clicking this box opens a new post editor. The post editor is customizable, but the basic editor provides fields for a post body and an optional post <b>Title</b>. Buttons below the text area to the left provide shortcuts to text formatting and inserting links, images, and other data into the post. The buttons to the right provide a post preview, the post permissions setting, and a <b>Submit</b> button to send the post.</dd> + <dt><a href='#' onclick='contextualHelp1("#group-sidebar", 1); return false;' title="Click to highlight element...">Privacy Groups</a></dt> + <dd>The privacy groups you have created are displayed in the side panel. Selecting them filters posts to those created by channels in the chosen group.</dd> + <dt><a href='#' onclick='$("#dbtn-acl").click(); return false;' title="Click to highlight element...">Post Permissions</a></dt> + <dd>The access control list (ACL) is what you use to set who can see your new post. Pressing the ACL button beside the Submit button will display a dialog in which you can select what channels and/or privacy groups can see the post. You can also select who is explicitly denied access. For example, say you are planning a surprise party for a friend. You can send an invitation post to everyone in your <b>Friends</b> group <i>except</i> the friend you are surprising. In this case you "show" the <b>Friends</b> group but "don't show" that one person.</dd> +</dl>
\ No newline at end of file diff --git a/doc/context/photos/help.html b/doc/context/photos/help.html new file mode 100644 index 000000000..f41611f8d --- /dev/null +++ b/doc/context/photos/help.html @@ -0,0 +1,22 @@ +<script> + var contextualHelp1 = function (target, openSidePanel) { + $("#help-content").removeClass('help-content-open'); // Close the help panel + $("#navbar-collapse-1").removeClass('in'); // Collapse the navbar for small screens + if (openSidePanel) { + $("main").addClass('region_1-on'); // Open the side panel to highlight element + } else { + $("main").removeClass('region_1-on'); + } + // Animate the page scroll to the element and then pulse the element to direct attention + $('html,body').animate({scrollTop: $(target).offset().top - $('#navbar-collapse-1').height() - 20}, 'slow'); + for (i = 0; i < 3; i++) { + $(target).fadeTo('slow', 0.1).fadeTo('slow', 1.0); + } + } +</script> +<dl class="dl-horizontal"> + <dt>General</dt> + <dd>This page displays a channel's photo albums. The images visible to the observer depend on the individual image permissions.</dd> + <dt><a href='#' onclick='contextualHelp1("#tabs-collapse-1", 0); return false;' title="Click to highlight element...">Channel Content Tabs</a></dt> + <dd>The channel content tabs are links to other content published by the channel. The <b>About</b> tab links to the channel profile. The <b>Photos</b> tab links to the channel photo galleries. The <b>Files</b> tab links to the general shared files published by the channel.</dd> +</dl>
\ No newline at end of file diff --git a/doc/context/profile/help.html b/doc/context/profile/help.html new file mode 100644 index 000000000..0d4abb8cb --- /dev/null +++ b/doc/context/profile/help.html @@ -0,0 +1,22 @@ +<script> + var contextualHelp1 = function (target, openSidePanel) { + $("#help-content").removeClass('help-content-open'); // Close the help panel + $("#navbar-collapse-1").removeClass('in'); // Collapse the navbar for small screens + if (openSidePanel) { + $("main").addClass('region_1-on'); // Open the side panel to highlight element + } else { + $("main").removeClass('region_1-on'); + } + // Animate the page scroll to the element and then pulse the element to direct attention + $('html,body').animate({scrollTop: $(target).offset().top - $('#navbar-collapse-1').height() - 20}, 'slow'); + for (i = 0; i < 3; i++) { + $(target).fadeTo('slow', 0.1).fadeTo('slow', 1.0); + } + } +</script> +<dl class="dl-horizontal"> + <dt>General</dt> + <dd>This is the profile page of a channel. It typically displays information describing the channel. If the channel represents a person in a social network, for example, then the profile might provide contact information and other personal details about the person. Channels can have multiple profiles, where the displayed profile depends on the observer.</dd> + <dt><a href='#' onclick='contextualHelp1("#tabs-collapse-1", 0); return false;' title="Click to highlight element...">Channel Content Tabs</a></dt> + <dd>The channel content tabs are links to other content published by the channel. The <b>About</b> tab links to the channel profile. The <b>Photos</b> tab links to the channel photo galleries. The <b>Files</b> tab links to the general shared files published by the channel.</dd> +</dl>
\ No newline at end of file diff --git a/doc/federate.bb b/doc/federate.bb index 5d253913d..9137ec160 100644 --- a/doc/federate.bb +++ b/doc/federate.bb @@ -8,7 +8,7 @@ There are three main components to writing federation plugins. These are: In addition, federation drivers must handle -[4] differences in privacy policies +[4] differences in privacy policies (and content formats) [h3]Making connections[/h3] @@ -36,8 +36,11 @@ Additional information that your plugin requires for communication can be stored When a connection is made, we generally call the notifier (include/notifier.php) to send a message to the remote channel. This is bound to the hook 'permissions_create'. Your plugin will need to handle this in order to send a "follow" or "make friends" message to the other network. -Note: The first stage zot lookup will be replaced with a webfinger lookup. This work is in progress. A separate lookup was required initially as webfinger does not allow non-SSL connections. We will provide non-SSL zot lookups (usually test and development sites) via the "old" XRD based webfinger to avoid this limitation. +Notes: The first stage zot lookup will be replaced with a webfinger lookup. This work is in progress. A separate lookup was required initially as webfinger does not allow non-SSL connections. We will provide non-SSL zot lookups (usually test and development sites) via the "old" XRD based webfinger to avoid this limitation. +The core application will attempt to create xchan records for projects identified as members of the "open web"; currently Hubzilla, Friendica, Diaspora, GNU-Social and Pump.io. This is so that comments can be passed amongst project sites and the network correctly identified. A federation plugin is required to fully federate with other networks, but comments may be passed to sites without such a plugin installed so that there are no unexplained holes in conversations. + +The core application must also provide signing ability for Diaspora comments since they require a special signing format and must be signed by the comment author regardless of whether that channel federates with Diaspora. The owner of the conversation may federate with Diaspora so the comments must be signed. This is unfortunate but necessary. [h3]Sending Messages[/h3] diff --git a/doc/filesync.md b/doc/filesync.md new file mode 100644 index 000000000..4c64bdb09 --- /dev/null +++ b/doc/filesync.md @@ -0,0 +1,61 @@ +File Sync and Clone +=================== + + + +File cloning across multiple instances of a channel is a very hard problem, due to the nature of PHP memory allocation. This needs to be handled dramatically differently than cloning or syncing of other information. (Processing one large video file or 40-50 photos could exhaust memory). Therefore we can't easily just dump all the data to a dump file and sequentially process it. Loading the dump file itself is likely to exhaust memory. + +There are also two primary operations we are considering. The first is the hardest - saving and then importing all your channel information into a new channel clone. The second is synchronising file changes as they occur across two or more "active" clones. + +For the first cut at this tool we will concentrate on the second case, while trying to maintain some measure of compatibility with the first case so that we can re-use the same tools. + +Meta Data +========= + + +First we need the metadata for the file in order to precisely re-construct its structure on another site. This requires the following information: + +'attach' structure (without file contents - which is the default) for the file itself **and** its parent directories so that we can re-create its precise place in the file system, since we do not know if the parent directory has been imported previously or ever. + +'photo' structure for any photo elements which were created as a result of uploading this file into the system. This typically contains several different 'scales' or thumbnail images, some of which may be cropped for profile photo use or cover photo use. We need to retain the cropping information which is not present in the metadata, but only in the stored data. The actual thumbnail image data may or may not be included in the metadata. A cover photo of large scale (scale #7) could potentially cause memory issues. Not as bad as a 100M video, but if you have several of these they could add up. + +'item' entries which are linked to this file. These can be file share activities, the "parent item" linked to photos, and any attached conversation items (photo likes, comments, etc.) + +All of these items will require URL replacement and re-signing of the item as they are relocated to another site. + + +File Data +========= + +Then we have the actual file data we need to reconstruct the file. This needs to be stored separately from the meta-data to avoid memory exhaustion when processing. The actual file data can be used to reconstruct the attach structure and the first four photo scales. If this is a photo, we need access to the "#4 scale" (profile photo) and the #7 scale (cover photo) as they were originally cropped. All other thumbnails can be generated from these. + + + +File Sync +========= + + +We will consider this operation first because it is probably the most straightforward to implement. When a photo is added to or removed or changed from the source system, we will send a clone sync packet to all known clones containing the metadata - but **no file data** . We can only send one sync packet per file operation that needs to be synced. + +The receiving end will create and perform URL translation on all the metadata structures and store them. Then it will need to fetch the actual data. Assuming CURL supports streaming, an authenticated request is sent to the original site and the original file is requested and streamed directly to disk (bypassing all processing). If photo scale #4 or scale #7 is required, these are requested and stored into their respective structures. We're assuming in this case that the cover photo large scale will not exhaust memory. If CURL cannot be made to support streaming, request packets need to be queued and sent to the origination site to obtain "chunks" of the file and re-assembled once all chunks have been retrieved. + +The authenticated request depends on the mechanism. For CURL streaming, some signed secret with a timestamp will probably need to be generated and posted to the file origination site. Then the data can be retrieved with minimal internal processing and dumped directly to disk using stdio buffering. In the case of a zot request, the zot request packet will be validated, however scheduling chunk batches and re-assembling them could be tricky. + + +File Backup/Restore +=================== + +This is much more complicated as we do not have an authenticate web server to request data from. The metadata can be mostly the same, but we need some form of signalling that we will not be fetching the file via the web. This will likely require a client side process to parse each metadata file and locate a file on disk which it is associated with. Then the data would need to be streamed to the destination server with a special endpoint designed for this task. A java app might be the best option here to retain platform neutrality. + +Another option would be to use WebDAV for this step. The metadata files would be uploaded first, and then the data files. If a data file corresponded to an existing metadata file, the metadata would be processed; the file stored appropriately, and the metadata file then removed. In this case, photos of scales 4 and 7 would need to be provided in the metadata. + + +Optionally, this step could also be performed with a filesystem local to the destination server. This would be the highest performance, and a suite of shell-based tools (in the case of Linux) could perform the "client-side" of the task. + +The complexity of this task mandates careful planning into how the data is organised and stored and if necessary backed up remotely or transmitted for backup by the source website. + + +Backward Compatibility +====================== + +There are some obvious issues with making data available for backup or cloning which existed on the system prior to the existence of restore/sync tools. To keep the tools themselves relatively uncomplicated (to the extent possible given the constraints) backward compatibility may have to be preformed by dedicated plugin or addon.
\ No newline at end of file diff --git a/include/Contact.php b/include/Contact.php index 507c922d0..e011c60c8 100644 --- a/include/Contact.php +++ b/include/Contact.php @@ -389,7 +389,7 @@ function channel_remove($channel_id, $local = true, $unset_session=false) { proc_run('php','include/directory.php',$channel_id); if($channel_id == local_channel() && $unset_session) { - nuke_session(); + \Zotlabs\Web\Session::nuke(); goaway(z_root()); } diff --git a/include/api.php b/include/api.php index 41837ad88..fd644947c 100644 --- a/include/api.php +++ b/include/api.php @@ -486,7 +486,7 @@ require_once('include/api_auth.php'); function api_account_logout(&$a, $type){ require_once('include/auth.php'); - nuke_session(); + \Zotlabs\Web\Session::nuke(); return api_apply_template("user", $type, array('$user' => null)); } diff --git a/include/attach.php b/include/attach.php index 4ecc273e9..ae4681994 100644 --- a/include/attach.php +++ b/include/attach.php @@ -864,6 +864,12 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { // This would've been called already with a success result in photos_upload() if it was a photo. call_hooks('photo_upload_end',$ret); } + + $sync = attach_export_data($channel,$hash); + + if($sync) + build_sync_packet($channel['channel_id'],array('file' => array($sync))); + return $ret; } @@ -1814,3 +1820,89 @@ function filepath_macro($s) { } +function attach_export_data($channel, $resource_id, $deleted = false) { + + $ret = array(); + + $paths = array(); + + $hash_ptr = $resource_id; + + $ret['fetch_url'] = z_root() . '/getfile'; + $ret['original_channel'] = $channel['channel_address']; + + + if($deleted) { + $ret['attach'] = array(array('hash' => $resource_id, 'deleted' => 1)); + return $ret; + } + + do { + $r = q("select * from attach where hash = '%s' and uid = %d limit 1", + dbesc($hash_ptr), + intval($channel['channel_id']) + ); + if(! $r) + break; + + if($hash_ptr === $resource_id) { + $attach_ptr = $r[0]; + } + + $hash_ptr = $r[0]['folder']; + $paths[] = $r[0]; + } while($hash_ptr); + + + + + $paths = array_reverse($paths); + + $ret['attach'] = $paths; + + + if($attach_ptr['is_photo']) { + $r = q("select * from photo where resource_id = '%s' and uid = %d order by scale asc", + dbesc($resource_id), + intval($channel['channel_id']) + ); + if($r) { + for($x = 0; $x < count($r); $x ++) { + $r[$x]['data'] = base64_encode($r[$x]['data']); + } + $ret['photo'] = $r; + } + + $r = q("select * from item where resource_id = '%s' and resource_type = 'photo' and uid = %d ", + dbesc($resource_id), + intval($channel['channel_id']) + ); + if($r) { + $ret['item'] = array(); + $items = q("select item.*, item.id as item_id from item where item.parent = %d ", + intval($r[0]['id']) + ); + if($items) { + xchan_query($items); + $items = fetch_post_tags($items,true); + foreach($items as $rr) + $ret['item'][] = encode_item($rr,true); + } + } + } + + return $ret; + +} + + +/* strip off 'store/nickname/' from the provided path */ + +function get_attach_binname($s) { + $p = $s; + if(strpos($s,'store/') === 0) { + $p = substr($s,6); + $p = substr($p,strpos($p,'/')+1); + } + return $p; +}
\ No newline at end of file diff --git a/include/auth.php b/include/auth.php index aaec45c40..9643da8eb 100644 --- a/include/auth.php +++ b/include/auth.php @@ -101,7 +101,7 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && // process logout request $args = array('channel_id' => local_channel()); call_hooks('logging_out', $args); - nuke_session(); + \Zotlabs\Web\Session::nuke(); info( t('Logged out.') . EOL); goaway(z_root()); } @@ -117,7 +117,7 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && intval(ACCOUNT_ROLE_ADMIN) ); if($x) { - new_cookie(60 * 60 * 24); // one day + \Zotlabs\Web\Session::new_cookie(60 * 60 * 24); // one day $_SESSION['last_login_date'] = datetime_convert(); unset($_SESSION['visitor_id']); // no longer a visitor authenticate_success($x[0], true, true); @@ -141,42 +141,7 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && if(x($_SESSION, 'uid') || x($_SESSION, 'account_id')) { - // first check if we're enforcing that sessions can't change IP address - // @todo what to do with IPv6 addresses - if($_SESSION['addr'] && $_SESSION['addr'] != $_SERVER['REMOTE_ADDR']) { - logger('SECURITY: Session IP address changed: ' . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); - - $partial1 = substr($_SESSION['addr'], 0, strrpos($_SESSION['addr'], '.')); - $partial2 = substr($_SERVER['REMOTE_ADDR'], 0, strrpos($_SERVER['REMOTE_ADDR'], '.')); - - $paranoia = intval(get_pconfig($_SESSION['uid'], 'system', 'paranoia')); - if(! $paranoia) - $paranoia = intval(get_config('system', 'paranoia')); - - switch($paranoia) { - case 0: - // no IP checking - break; - case 2: - // check 2 octets - $partial1 = substr($partial1, 0, strrpos($partial1, '.')); - $partial2 = substr($partial2, 0, strrpos($partial2, '.')); - if($partial1 == $partial2) - break; - case 1: - // check 3 octets - if($partial1 == $partial2) - break; - case 3: - default: - // check any difference at all - logger('Session address changed. Paranoid setting in effect, blocking session. ' - . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); - nuke_session(); - goaway(z_root()); - break; - } - } + Zotlabs\Web\Session::return_check(); $r = q("select * from account where account_id = %d limit 1", intval($_SESSION['account_id']) @@ -190,13 +155,14 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && } if(strcmp(datetime_convert('UTC','UTC','now - 12 hours'), $_SESSION['last_login_date']) > 0 ) { $_SESSION['last_login_date'] = datetime_convert(); + Zotlabs\Web\Session::extend_cookie(); $login_refresh = true; } authenticate_success($r[0], false, false, false, $login_refresh); } else { $_SESSION['account_id'] = 0; - nuke_session(); + \Zotlabs\Web\Session::nuke(); goaway(z_root()); } } // end logged in user returning @@ -204,7 +170,7 @@ if((isset($_SESSION)) && (x($_SESSION, 'authenticated')) && else { if(isset($_SESSION)) { - nuke_session(); + \Zotlabs\Web\Session::nuke(); } // handle a fresh login request @@ -246,7 +212,7 @@ else { notice( t('Failed authentication') . EOL); } - logger('authenticate: ' . print_r(App::$account, true), LOGGER_DEBUG); + logger('authenticate: ' . print_r(App::$account, true), LOGGER_ALL); } if((! $record) || (! count($record))) { @@ -274,11 +240,13 @@ else { // (i.e. expire when the browser is closed), even when there's a time expiration // on the cookie - if($_POST['remember']) { - new_cookie(31449600); // one year + if($_POST['remember_me']) { + $_SESSION['remember_me'] = 1; + \Zotlabs\Web\Session::new_cookie(31449600); // one year } else { - new_cookie(0); // 0 means delete on browser exit + $_SESSION['remember_me'] = 0; + \Zotlabs\Web\Session::new_cookie(0); // 0 means delete on browser exit } // if we haven't failed up this point, log them in. diff --git a/include/bbcode.php b/include/bbcode.php index c2ff5d7c8..78a2759c1 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -243,9 +243,7 @@ function bb_ShareAttributes($match) { if ($matches[1] != "") $message_id = $matches[1]; - - /** @FIXME - this should really be a wall-item-ago so it will get updated on the client */ - $reldate = (($posted) ? relative_date($posted) : ''); + $reldate = '<span class="autotime" title="' . datetime_convert('UTC', date_default_timezone_get(), $posted, 'c') . '" >' . datetime_convert('UTC', date_default_timezone_get(), $posted, 'r') . '</span>'; $headline = '<div class="shared_container"> <div class="shared_header">'; diff --git a/include/cli_startup.php b/include/cli_startup.php index b0e4fcf10..a99164d4c 100644 --- a/include/cli_startup.php +++ b/include/cli_startup.php @@ -30,7 +30,7 @@ function cli_startup() { unset($db_host, $db_port, $db_user, $db_pass, $db_data, $db_type); }; - require_once('include/session.php'); + \Zotlabs\Web\Session::init(); load_config('system'); diff --git a/include/conversation.php b/include/conversation.php index 829e85382..7d80b08fc 100644 --- a/include/conversation.php +++ b/include/conversation.php @@ -1173,7 +1173,9 @@ function status_editor($a, $x, $popup = false) { '$term' => t('Tag term:'), '$fileas' => t('Save to Folder:'), '$whereareu' => t('Where are you right now?'), - '$expireswhen' => t('Expires YYYY-MM-DD HH:MM') + '$expireswhen' => t('Expires YYYY-MM-DD HH:MM'), + '$editor_autocomplete'=> ((x($x,'editor_autocomplete')) ? $x['editor_autocomplete'] : ''), + '$bbco_autocomplete'=> ((x($x,'bbco_autocomplete')) ? $x['bbco_autocomplete'] : ''), )); $tpl = get_markup_template('jot.tpl'); @@ -1274,6 +1276,7 @@ function status_editor($a, $x, $popup = false) { '$expiryModalOK' => t('OK'), '$expiryModalCANCEL' => t('Cancel'), '$expanded' => ((x($x, 'expanded')) ? $x['expanded'] : false), + '$bbcode' => ((x($x, 'bbcode')) ? $x['bbcode'] : false) )); if ($popup === true) { @@ -1592,6 +1595,7 @@ function network_tabs() { function profile_tabs($a, $is_owner = false, $nickname = null){ // Don't provide any profile tabs if we're running as the sys channel + if (App::$is_sys) return; @@ -1669,7 +1673,7 @@ function profile_tabs($a, $is_owner = false, $nickname = null){ } - if ($p['chat']) { + if ($p['chat'] && feature_enabled($uid,'ajaxchat')) { require_once('include/chat.php'); $has_chats = chatroom_list_count($uid); if ($has_chats) { diff --git a/include/features.php b/include/features.php index ff6b71d4c..38700f9f5 100644 --- a/include/features.php +++ b/include/features.php @@ -56,7 +56,7 @@ function get_features($filtered = true) { array('private_notes', t('Private Notes'), t('Enables a tool to store notes and reminders (note: not encrypted)'),false,get_config('feature_lock','private_notes')), array('nav_channel_select', t('Navigation Channel Select'), t('Change channels directly from within the navigation dropdown menu'),false,get_config('feature_lock','nav_channel_select')), array('photo_location', t('Photo Location'), t('If location data is available on uploaded photos, link this to a map.'),false,get_config('feature_lock','photo_location')), - + array('ajaxchat', t('Access Controlled Chatrooms'), t('Provide chatrooms and chat services with access control.'),true,get_config('feature_lock','ajaxchat')), array('smart_birthdays', t('Smart Birthdays'), t('Make birthday events timezone aware in case your friends are scattered across the planet.'),true,get_config('feature_lock','smart_birthdays')), array('expert', t('Expert Mode'), t('Enable Expert Mode to provide advanced configuration options'),false,get_config('feature_lock','expert')), array('premium_channel', t('Premium Channel'), t('Allows you to set restrictions and terms on those that connect with your channel'),false,get_config('feature_lock','premium_channel')), diff --git a/include/identity.php b/include/identity.php index 849742c8e..9bb4fb3c5 100644 --- a/include/identity.php +++ b/include/identity.php @@ -550,7 +550,8 @@ function identity_basic_export($channel_id, $items = false) { if($r) $ret['config'] = $r; - $r = q("select type, data, os_storage from photo where scale = 4 and profile = 1 and uid = %d limit 1", + $r = q("select type, data, os_storage from photo where scale = 4 and photo_usage = %d and uid = %d limit 1", + intval(PHOTO_PROFILE), intval($channel_id) ); diff --git a/include/import.php b/include/import.php index 9a57012b2..3b5c8508c 100644 --- a/include/import.php +++ b/include/import.php @@ -870,6 +870,257 @@ function import_mail($channel,$mails) { +function sync_files($channel,$files) { + require_once('include/attach.php'); + + if($channel && $files) { + foreach($files as $f) { + if(! $f) + continue; + + $fetch_url = $f['fetch_url']; + $oldbase = dirname($fetch_url); + $original_channel = $f['original_channel']; + + if(! ($fetch_url && $original_channel)) + continue; + + if($f['attach']) { + $attachment_stored = false; + foreach($f['attach'] as $att) { + + if($att['deleted']) { + attach_delete($channel,$att['hash']); + continue; + } + + $attach_exists = false; + $x = attach_by_hash($att['hash']); + + if($x) { + $attach_exists = true; + $attach_id = $x[0]['id']; + } + + $newfname = 'store/' . $channel['channel_address'] . '/' . get_attach_binname($att['data']); + + unset($att['id']); + $att['aid'] = $channel['channel_account_id']; + $att['uid'] = $channel['channel_id']; + + + // check for duplicate folder names with the same parent. + // If we have a duplicate that doesn't match this hash value + // change the name so that the contents won't be "covered over" + // by the existing directory. Use the same logic we use for + // duplicate files. + + if(strpos($att['filename'],'.') !== false) { + $basename = substr($att['filename'],0,strrpos($att['filename'],'.')); + $ext = substr($att['filename'],strrpos($att['filename'],'.')); + } + else { + $basename = $att['filename']; + $ext = ''; + } + + $r = q("select filename from attach where ( filename = '%s' OR filename like '%s' ) and folder = '%s' and hash != '%s' ", + dbesc($basename . $ext), + dbesc($basename . '(%)' . $ext), + dbesc($att['folder']), + dbesc($att['hash']) + ); + + if($r) { + $x = 1; + + do { + $found = false; + foreach($r as $rr) { + if($rr['filename'] === $basename . '(' . $x . ')' . $ext) { + $found = true; + break; + } + } + if($found) + $x++; + } + while($found); + $att['filename'] = $basename . '(' . $x . ')' . $ext; + } + else + $att['filename'] = $basename . $ext; + + // end duplicate detection + +// @fixme - update attachment structures if they are modified rather than created + + $att['data'] = $newfname; + + // Note: we use $att['hash'] below after it has been escaped to + // fetch the file contents. + // If the hash ever contains any escapable chars this could cause + // problems. Currently it does not. + + dbesc_array($att); + + + if($attach_exists) { + $str = ''; + foreach($att as $k => $v) { + if($str) + $str .= ","; + $str .= " `" . $k . "` = '" . $v . "' "; + } + $r = dbq("update `attach` set " . $str . " where id = " . intval($attach_id) ); + } + else { + $r = dbq("INSERT INTO attach (`" + . implode("`, `", array_keys($att)) + . "`) VALUES ('" + . implode("', '", array_values($att)) + . "')" ); + } + + + // is this a directory? + + if($att['filetype'] === 'multipart/mixed' && $att['is_dir']) { + os_mkdir($newfname, STORAGE_DEFAULT_PERMISSIONS,true); + continue; + } + else { + + // it's a file + // for the sync version of this algorithm (as opposed to 'offline import') + // we will fetch the actual file from the source server so it can be + // streamed directly to disk and avoid consuming PHP memory if it's a huge + // audio/video file or something. + + $time = datetime_convert(); + + $parr = array('hash' => $channel['channel_hash'], + 'time' => $time, + 'resource' => $att['hash'], + 'revision' => 0, + 'signature' => base64url_encode(rsa_sign($channel['channel_hash'] . '.' . $time, $channel['channel_prvkey'])) + ); + + $store_path = $newfname; + + $fp = fopen($newfname,'w'); + if(! $fp) { + logger('failed to open storage file.',LOGGER_NORMAL,LOG_ERR); + continue; + } + $redirects = 0; + $x = z_post_url($fetch_url,$parr,$redirects,array('filep' => $fp)); + fclose($fp); + + if($x['success']) { + $attachment_stored = true; + } + continue; + } + } + } + if(! $attachment_stored) { + // @TODO should we queue this and retry or delete everything or what? + logger('attachment store failed',LOGGER_NORMAL,LOG_ERR); + } + if($f['photo']) { + foreach($f['photo'] as $p) { + unset($p['id']); + $p['aid'] = $channel['channel_account_id']; + $p['uid'] = $channel['channel_id']; + + // if this is a profile photo, undo the profile photo bit + // for any other photo which previously held it. + + if($p['photo_usage'] == PHOTO_PROFILE) { + $e = q("update photo set photo_usage = %d where photo_usage = %d + and resource_id != '%s' and uid = %d ", + intval(PHOTO_NORMAL), + intval(PHOTO_PROFILE), + dbesc($p['resource_id']), + intval($channel['channel_id']) + ); + } + + // same for cover photos + + if($p['photo_usage'] == PHOTO_COVER) { + $e = q("update photo set photo_usage = %d where photo_usage = %d + and resource_id != '%s' and uid = %d ", + intval(PHOTO_NORMAL), + intval(PHOTO_COVER), + dbesc($p['resource_id']), + intval($channel['channel_id']) + ); + } + + if($p['scale'] === 0 && $p['os_storage']) + $p['data'] = $store_path; + else + $p['data'] = base64_decode($p['data']); + + + $exists = q("select * from photo where resource_id = '%s' and scale = %d and uid = %d limit 1", + dbesc($p['resource_id']), + intval($p['scale']), + intval($channel['channel_id']) + ); + + dbesc_array($p); + + if($exists) { + $str = ''; + foreach($p as $k => $v) { + if($str) + $str .= ","; + $str .= " `" . $k . "` = '" . $v . "' "; + } + $r = dbq("update `photo` set " . $str . " where id = " . intval($exists[0]['id']) ); + } + else { + $r = dbq("INSERT INTO photo (`" + . implode("`, `", array_keys($p)) + . "`) VALUES ('" + . implode("', '", array_values($p)) + . "')" ); + } + } + } + if($f['item']) { + sync_items($channel,$f['item']); + foreach($f['item'] as $i) { + if($i['message_id'] !== $i['message_parent']) + continue; + $r = q("select * from item where mid = '%s' and uid = %d limit 1", + dbesc($i['message_id']), + intval($channel['channel_id']) + ); + if($r) { + $item = $r[0]; + item_url_replace($channel,$item,$oldbase,z_root(),$original_channel); + + dbesc_array($item); + $item_id = $item['id']; + unset($item['id']); + $str = ''; + foreach($item as $k => $v) { + if($str) + $str .= ","; + $str .= " `" . $k . "` = '" . $v . "' "; + } + + $r = dbq("update `item` set " . $str . " where id = " . $item_id ); + } + } + } + } + } +} diff --git a/include/items.php b/include/items.php index 07cf2e0e8..95822c0ba 100755 --- a/include/items.php +++ b/include/items.php @@ -4118,22 +4118,26 @@ function feed_meta($xml) { $rawauthor = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10,'author'); logger('rawauthor: ' . print_r($rawauthor,true)); - if($rawauthor && $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']) { - $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; - foreach($base as $link) { - if(!x($author, 'author_photo') || ! $author['author_photo']) { - if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar') { - $author['author_photo'] = unxmlify($link['attribs']['']['href']); - break; - } - } - } + if($rawauthor) { + if($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']) { + $base = $rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['link']; + foreach($base as $link) { + if(!x($author, 'author_photo') || ! $author['author_photo']) { + if($link['attribs']['']['rel'] === 'photo' || $link['attribs']['']['rel'] === 'avatar') { + $author['author_photo'] = unxmlify($link['attribs']['']['href']); + break; + } + } + } + } if($rawauthor[0]['child'][NAMESPACE_POCO]['displayName'][0]['data']) $author['full_name'] = unxmlify($rawauthor[0]['child'][NAMESPACE_POCO]['displayName'][0]['data']); + if($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']) + $author['author_uri'] = unxmlify($rawauthor[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']); + } } - if(substr($author['author_link'],-1,1) == '/') $author['author_link'] = substr($author['author_link'],0,-1); diff --git a/include/nav.php b/include/nav.php index 3f5c7963a..541ab3aed 100644 --- a/include/nav.php +++ b/include/nav.php @@ -92,7 +92,7 @@ EOT; $nav['usermenu'][] = Array('photos/' . $channel['channel_address'], t('Photos'), "", t('Your photos'),'photos_nav_btn'); $nav['usermenu'][] = Array('cloud/' . $channel['channel_address'],t('Files'),"",t('Your files'),'cloud_nav_btn'); - if(! UNO) + if((! UNO) && feature_enabled(local_channel(),'ajaxchat')) $nav['usermenu'][] = Array('chat/' . $channel['channel_address'], t('Chat'),"",t('Your chatrooms'),'chat_nav_btn'); @@ -149,9 +149,11 @@ EOT; $help_url = z_root() . '/help?f=&cmd=' . App::$cmd; - if(! get_config('system','hide_help')) - $nav['help'] = array($help_url, t('Help'), "", t('Help and documentation'),'help_nav_btn'); - + if(! get_config('system','hide_help')) { + require_once('mod/help.php'); + $context_help = load_context_help(); + $nav['help'] = array($help_url, t('Help'), "", t('Help and documentation'),'help_nav_btn',$context_help); + } if(! UNO) $nav['apps'] = array('apps', t('Apps'), "", t('Applications, utilities, links, games'),'apps_nav_btn'); @@ -256,7 +258,6 @@ $powered_by = ''; * */ function nav_set_selected($item){ - $a = get_app(); App::$nav_sel = array( 'community' => null, 'network' => null, diff --git a/include/network.php b/include/network.php index ac14548a5..ec255581d 100644 --- a/include/network.php +++ b/include/network.php @@ -27,11 +27,12 @@ function get_capath() { * * \b http_auth => username:password * * \b novalidate => do not validate SSL certs, default is to validate using our CA list * * \b nobody => only return the header + * * \b filep => stream resource to write body to. header and body are not returned when using this option. * * @return array an assoziative array with: * * \e int \b return_code => HTTP return code or 0 if timeout or failure * * \e boolean \b success => boolean true (if HTTP 2xx result) or false - * * \e string \b header => HTTP headers + * * \e string \b header => HTTP headers * * \e string \b body => fetched content */ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { @@ -53,6 +54,11 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { if($ciphers) @curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, $ciphers); + if(x($opts,'filep')) { + @curl_setopt($ch, CURLOPT_FILE, $opts['filep']); + @curl_setopt($ch, CURLOPT_HEADER, $false); + } + if(x($opts,'headers')) @curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['headers']); @@ -158,6 +164,7 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { * 'timeout' => int seconds, default system config value or 60 seconds * 'http_auth' => username:password * 'novalidate' => do not validate SSL certs, default is to validate using our CA list + * 'filep' => stream resource to write body to. header and body are not returned when using this option. * @return array an assoziative array with: * * \e int \b return_code => HTTP return code or 0 if timeout or failure * * \e boolean \b success => boolean true (if HTTP 2xx result) or false @@ -185,6 +192,11 @@ function z_post_url($url,$params, $redirects = 0, $opts = array()) { if($ciphers) @curl_setopt($ch, CURLOPT_SSL_CIPHER_LIST, $ciphers); + if(x($opts,'filep')) { + @curl_setopt($ch, CURLOPT_FILE, $opts['filep']); + @curl_setopt($ch, CURLOPT_HEADER, $false); + } + if(x($opts,'headers')) { @curl_setopt($ch, CURLOPT_HTTPHEADER, $opts['headers']); logger('headers: ' . print_r($opts['headers'],true) . 'redir: ' . $redirects); @@ -1089,7 +1101,7 @@ function discover_by_webbie($webbie) { // If we discover zot - don't search further; grab the info and get out of // here. - if($link['rel'] == 'http://purl.org/zot/protocol') { + if($link['rel'] === PROTOCOL_ZOT) { logger('discover_by_webbie: zot found for ' . $webbie, LOGGER_DEBUG); if(array_key_exists('zot',$x) && $x['zot']['success']) $i = import_xchan($x['zot']); @@ -1265,6 +1277,12 @@ function discover_by_webbie($webbie) { if($feed_meta['author']['author_photo']) $avatar = $feed_meta['author']['author_photo']; } + + // for GNU-social over-ride any url aliases we may have picked up in webfinger + // The author.uri element in the feed is likely to be more accurate + + if($gnusoc && $feed_meta['author']['author_uri']) + $location = $feed_meta['author']['author_uri']; } } else { diff --git a/include/photos.php b/include/photos.php index 5838c013f..943d7d503 100644 --- a/include/photos.php +++ b/include/photos.php @@ -194,7 +194,7 @@ function photo_upload($channel, $observer, $args) { $link[0] = array( 'rel' => 'alternate', 'type' => 'text/html', - 'href' => $url = rawurlencode(z_root() . '/photo/' . $photo_hash . '-0.' . $ph->getExt()), + 'href' => z_root() . '/photo/' . $photo_hash . '-0.' . $ph->getExt(), 'width' => $ph->getWidth(), 'height' => $ph->getHeight() ); @@ -212,7 +212,7 @@ function photo_upload($channel, $observer, $args) { $link[1] = array( 'rel' => 'alternate', 'type' => 'text/html', - 'href' => $url = rawurlencode(z_root() . '/photo/' . $photo_hash . '-1.' . $ph->getExt()), + 'href' => z_root() . '/photo/' . $photo_hash . '-1.' . $ph->getExt(), 'width' => $ph->getWidth(), 'height' => $ph->getHeight() ); @@ -227,7 +227,7 @@ function photo_upload($channel, $observer, $args) { $link[2] = array( 'rel' => 'alternate', 'type' => 'text/html', - 'href' => $url = rawurlencode(z_root() . '/photo/' . $photo_hash . '-2.' . $ph->getExt()), + 'href' => z_root() . '/photo/' . $photo_hash . '-2.' . $ph->getExt(), 'width' => $ph->getWidth(), 'height' => $ph->getHeight() ); @@ -242,7 +242,7 @@ function photo_upload($channel, $observer, $args) { $link[3] = array( 'rel' => 'alternate', 'type' => 'text/html', - 'href' => $url = rawurlencode(z_root() . '/photo/' . $photo_hash . '-3.' . $ph->getExt()), + 'href' => z_root() . '/photo/' . $photo_hash . '-3.' . $ph->getExt(), 'width' => $ph->getWidth(), 'height' => $ph->getHeight() ); @@ -312,7 +312,7 @@ function photo_upload($channel, $observer, $args) { 'title' => $title, 'created' => $p['created'], 'edited' => $p['edited'], - 'id' => rawurlencode(z_root() . '/photos/' . $channel['channel_address'] . '/image/' . $photo_hash), + 'id' => z_root() . '/photos/' . $channel['channel_address'] . '/image/' . $photo_hash, 'link' => $link, 'body' => $obj_body ); @@ -320,7 +320,7 @@ function photo_upload($channel, $observer, $args) { $target = array( 'type' => ACTIVITY_OBJ_ALBUM, 'title' => (($album) ? $album : '/'), - 'id' => rawurlencode(z_root() . '/photos/' . $channel['channel_address'] . '/album/' . bin2hex($album)) + 'id' => z_root() . '/photos/' . $channel['channel_address'] . '/album/' . bin2hex($album) ); // Create item container diff --git a/include/reddav.php b/include/reddav.php index a0bd1b1fc..abf21b86d 100644 --- a/include/reddav.php +++ b/include/reddav.php @@ -23,9 +23,6 @@ use Zotlabs\Storage; require_once('vendor/autoload.php'); require_once('include/attach.php'); -//require_once('Zotlabs/Storage/File.php'); -//require_once('Zotlabs/Storage/Directory.php'); -//require_once('Zotlabs/Storage/BasicAuth.php'); /** * @brief Returns an array with viewable channels. diff --git a/include/session.php b/include/session.php index 71bfdc12a..4a7c8052e 100644 --- a/include/session.php +++ b/include/session.php @@ -45,12 +45,13 @@ function nuke_session() { function new_cookie($time) { + $old_sid = session_id(); // ??? This shouldn't have any effect if called after session_start() // We probably need to set the session expiration and change the PHPSESSID cookie. + // session_set_cookie_params($time); - session_set_cookie_params($time); session_regenerate_id(false); q("UPDATE session SET sid = '%s' WHERE sid = '%s'", @@ -66,6 +67,7 @@ function new_cookie($time) { } setcookie('jsAvailable', $_COOKIE['jsAvailable'], $expires); } + setcookie(session_name(),session_id(),$expires); } diff --git a/include/text.php b/include/text.php index a1a1cfb1c..0a7f84b01 100644 --- a/include/text.php +++ b/include/text.php @@ -2730,22 +2730,37 @@ function json_url_replace($old,$new,&$s) { } -function item_url_replace($channel,&$item,$old,$new) { +function item_url_replace($channel,&$item,$old,$new,$oldnick = '') { - if($item['attach']) + if($item['attach']) { json_url_replace($old,$new,$item['attach']); - if($item['object']) + if($oldnick) + json_url_replace('/' . $oldnick . '/' ,'/' . $channel['channel_address'] . '/' ,$item['attach']); + } + if($item['object']) { json_url_replace($old,$new,$item['object']); - if($item['target']) + if($oldnick) + json_url_replace('/' . $oldnick . '/' ,'/' . $channel['channel_address'] . '/' ,$item['object']); + } + if($item['target']) { json_url_replace($old,$new,$item['target']); + if($oldnick) + json_url_replace('/' . $oldnick . '/' ,'/' . $channel['channel_address'] . '/' ,$item['target']); + } if(string_replace($old,$new,$item['body'])) { $item['sig'] = base64url_encode(rsa_sign($item['body'],$channel['channel_prvkey'])); $item['item_verified'] = 1; } - // @fixme item['plink'] and item['llink'] + $item['plink'] = str_replace($old,$new,$item['plink']); + if($oldnick) + $item['plink'] = str_replace('/' . $oldnick . '/' ,'/' . $channel['channel_address'] . '/' ,$item['plink']); + $item['llink'] = str_replace($old,$new,$item['llink']); + if($oldnick) + $item['llink'] = str_replace('/' . $oldnick . '/' ,'/' . $channel['channel_address'] . '/' ,$item['llink']); + } diff --git a/include/widgets.php b/include/widgets.php index 0116e5bd1..0355ebd8c 100644 --- a/include/widgets.php +++ b/include/widgets.php @@ -785,6 +785,7 @@ function widget_menu_preview($arr) { function widget_chatroom_list($arr) { + require_once("include/chat.php"); $r = chatroom_list(App::$profile['profile_uid']); @@ -808,6 +809,10 @@ function widget_chatroom_members() { } function widget_bookmarkedchats($arr) { + + if(! feature_enabled(App::$profile['profile_uid'],'ajaxchat')) + return ''; + $h = get_observer_hash(); if(! $h) return; @@ -827,6 +832,9 @@ function widget_bookmarkedchats($arr) { function widget_suggestedchats($arr) { + if(! feature_enabled(App::$profile['profile_uid'],'ajaxchat')) + return ''; + // probably should restrict this to your friends, but then the widget will only work // if you are logged in locally. diff --git a/include/zot.php b/include/zot.php index fa3563085..a5ab56154 100644 --- a/include/zot.php +++ b/include/zot.php @@ -3138,6 +3138,9 @@ function process_channel_sync_delivery($sender, $arr, $deliveries) { if(array_key_exists('menu',$arr) && $arr['menu']) sync_menus($channel,$arr['menu']); + if(array_key_exists('file',$arr) && $arr['file']) + sync_files($channel,$arr['file']); + if(array_key_exists('channel',$arr) && is_array($arr['channel']) && count($arr['channel'])) { if(array_key_exists('channel_pageflags',$arr['channel']) && intval($arr['channel']['channel_pageflags'])) { @@ -62,7 +62,7 @@ if(! App::$install) { load_config('system'); load_config('feature'); - require_once('include/session.php'); + \Zotlabs\Web\Session::init(); load_hooks(); call_hooks('init_1'); @@ -84,7 +84,7 @@ if(! App::$install) { * */ -session_start(); +\Zotlabs\Web\Session::start(); /** * Language was set earlier, but we can over-ride it in the session. @@ -190,5 +190,4 @@ call_hooks('page_end', App::$page['content']); construct_page($a); -session_write_close(); exit; diff --git a/install/htconfig.sample.php b/install/htconfig.sample.php index 1d9dc1a13..5e506225e 100755 --- a/install/htconfig.sample.php +++ b/install/htconfig.sample.php @@ -53,6 +53,7 @@ App::$config['system']['location_hash'] = 'if the auto install failed, put a uni App::$config['system']['transport_security_header'] = 1; App::$config['system']['content_security_policy'] = 1; +App::$config['system']['ssl_cookie_protection'] = 1; // Your choices are REGISTER_OPEN, REGISTER_APPROVE, or REGISTER_CLOSED. diff --git a/library/justifiedGallery/jquery.justifiedGallery.js b/library/justifiedGallery/jquery.justifiedGallery.js index 7c63149a3..d3259fe44 100644 --- a/library/justifiedGallery/jquery.justifiedGallery.js +++ b/library/justifiedGallery/jquery.justifiedGallery.js @@ -1,5 +1,5 @@ /*! - * Justified Gallery - v3.6.0 + * Justified Gallery - v3.6.1 * http://miromannino.github.io/Justified-Gallery/ * Copyright (c) 2015 Miro Mannino * Licensed under the MIT license. @@ -23,6 +23,7 @@ this.buildingRow = { entriesBuff : [], width : 0, + height : 0, aspectRatio : 0 }; this.lastAnalyzedIndex = -1; @@ -97,11 +98,18 @@ * @returns {String} the suffix to use */ JustifiedGallery.prototype.newSrc = function (imageSrc, imgWidth, imgHeight) { - var matchRes = imageSrc.match(this.settings.extension); - var ext = (matchRes != null) ? matchRes[0] : ''; - var newImageSrc = imageSrc.replace(this.settings.extension, ''); - newImageSrc = this.removeSuffix(newImageSrc, this.getUsedSuffix(newImageSrc)); - newImageSrc += this.getSuffix(imgWidth, imgHeight) + ext; + var newImageSrc; + + if (this.settings.thumbnailPath) { + newImageSrc = this.settings.thumbnailPath(imageSrc, imgWidth, imgHeight); + } else { + var matchRes = imageSrc.match(this.settings.extension); + var ext = (matchRes !== null) ? matchRes[0] : ''; + newImageSrc = imageSrc.replace(this.settings.extension, ''); + newImageSrc = this.removeSuffix(newImageSrc, this.getUsedSuffix(newImageSrc)); + newImageSrc += this.getSuffix(imgWidth, imgHeight) + ext; + } + return newImageSrc; }; @@ -211,10 +219,10 @@ var $imgCaption = this.captionFromEntry($entry); // Create it if it doesn't exists - if ($imgCaption == null) { + if ($imgCaption === null) { var caption = $image.attr('alt'); - if (typeof caption === 'undefined') caption = $entry.attr('title'); - if (typeof caption !== 'undefined') { // Create only we found something + if (!this.isValidCaption(caption)) caption = $entry.attr('title'); + if (this.isValidCaption(caption)) { // Create only we found something $imgCaption = $('<div class="caption">' + caption + '</div>'); $entry.append($imgCaption); $entry.data('jg.createdCaption', true); @@ -232,6 +240,16 @@ }; /** + * Validates the caption + * + * @param caption The caption that should be validated + * @return {boolean} Validation result + */ + JustifiedGallery.prototype.isValidCaption = function (caption) { + return (typeof caption !== 'undefined' && caption.length > 0); + }; + + /** * The callback for the event 'mouseenter'. It assumes that the event currentTarget is an entry. * It shows the caption using jQuery (or using CSS if it is configured so) * @@ -299,7 +317,7 @@ * Justify the building row, preparing it to * * @param isLastRow - * @returns {*} + * @returns a boolean to know if the row has been justified or not */ JustifiedGallery.prototype.prepareBuildingRow = function (isLastRow) { var i, $entry, imgAspectRatio, newImgW, newImgH, justify = true; @@ -322,7 +340,7 @@ } // With lastRow = nojustify, justify if is justificable (the images will not become too big) - if (isLastRow && !justifiable && this.settings.lastRow === 'nojustify') justify = false; + if (isLastRow && !justifiable && this.settings.lastRow !== 'justify' && this.settings.lastRow !== 'hide') justify = false; for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { $entry = this.buildingRow.entriesBuff[i]; @@ -355,7 +373,8 @@ if (this.settings.fixedHeight && minHeight > this.settings.rowHeight) minHeight = this.settings.rowHeight; - return {minHeight: minHeight, justify: justify}; + this.buildingRow.height = minHeight; + return justify; }; /** @@ -374,33 +393,53 @@ */ JustifiedGallery.prototype.flushRow = function (isLastRow) { var settings = this.settings; - var $entry, minHeight, buildingRowRes, offX = this.border; + var $entry, buildingRowRes, offX = this.border, i; buildingRowRes = this.prepareBuildingRow(isLastRow); - minHeight = buildingRowRes.minHeight; - if (isLastRow && settings.lastRow === 'hide' && minHeight === -1) { + if (isLastRow && settings.lastRow === 'hide' && this.buildingRow.height === -1) { this.clearBuildingRow(); return; } - if (this.maxRowHeight.percentage) { - if (this.maxRowHeight.value * settings.rowHeight < minHeight) minHeight = this.maxRowHeight.value * settings.rowHeight; + if (this.maxRowHeight.isPercentage) { + if (this.maxRowHeight.value * settings.rowHeight < this.buildingRow.height) { + this.buildingRow.height = this.maxRowHeight.value * settings.rowHeight; + } } else { - if (this.maxRowHeight.value > 0 && this.maxRowHeight.value < minHeight) minHeight = this.maxRowHeight.value; + if (this.maxRowHeight.value > 0 && this.maxRowHeight.value < this.buildingRow.height) { + this.buildingRow.height = this.maxRowHeight.value; + } } - for (var i = 0; i < this.buildingRow.entriesBuff.length; i++) { + //Align last (unjustified) row + if (settings.lastRow === 'center' || settings.lastRow === 'right') { + var availableWidth = this.galleryWidth - 2 * this.border - (this.buildingRow.entriesBuff.length - 1) * settings.margins; + + for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { + $entry = this.buildingRow.entriesBuff[i]; + availableWidth -= $entry.data('jg.jwidth'); + } + + if (settings.lastRow === 'center') + offX += availableWidth / 2; + else if (settings.lastRow === 'right') + offX += availableWidth; + } + + + for (i = 0; i < this.buildingRow.entriesBuff.length; i++) { $entry = this.buildingRow.entriesBuff[i]; - this.displayEntry($entry, offX, this.offY, $entry.data('jg.jwidth'), $entry.data('jg.jheight'), minHeight); + this.displayEntry($entry, offX, this.offY, $entry.data('jg.jwidth'), $entry.data('jg.jheight'), this.buildingRow.height); offX += $entry.data('jg.jwidth') + settings.margins; } //Gallery Height - this.$gallery.height(this.offY + minHeight + this.border + (this.isSpinnerActive() ? this.getSpinnerHeight() : 0)); + this.$gallery.height(this.offY + this.buildingRow.height + + this.border + (this.isSpinnerActive() ? this.getSpinnerHeight() : 0)); - if (!isLastRow || (minHeight <= this.settings.rowHeight && buildingRowRes.justify)) { + if (!isLastRow || (this.buildingRow.height <= settings.rowHeight && buildingRowRes)) { //Ready for a new row - this.offY += minHeight + this.settings.margins; + this.offY += this.buildingRow.height + settings.margins; this.clearBuildingRow(); this.$gallery.trigger('jg.rowflush'); } @@ -411,8 +450,8 @@ */ JustifiedGallery.prototype.checkWidth = function () { this.checkWidthIntervalId = setInterval($.proxy(function () { - var galleryWidth = parseInt(this.$gallery.width(), 10); - if (this.galleryWidth !== galleryWidth) { + var galleryWidth = parseFloat(this.$gallery.width()); + if (Math.abs(galleryWidth - this.galleryWidth) > this.settings.refreshSensitivity) { this.galleryWidth = galleryWidth; this.rewind(); @@ -426,7 +465,7 @@ * @returns {boolean} a boolean saying if the spinner is active or not */ JustifiedGallery.prototype.isSpinnerActive = function () { - return this.spinner.intervalId != null; + return this.spinner.intervalId !== null; }; /** @@ -454,7 +493,7 @@ var $spinnerPoints = spinnerContext.$el.find('span'); clearInterval(spinnerContext.intervalId); this.$gallery.append(spinnerContext.$el); - this.$gallery.height(this.offY + this.getSpinnerHeight()); + this.$gallery.height(this.offY + this.buildingRow.height + this.getSpinnerHeight()); spinnerContext.intervalId = setInterval(function () { if (spinnerContext.phase < $spinnerPoints.length) { $spinnerPoints.eq(spinnerContext.phase).fadeTo(spinnerContext.timeSlot, 1); @@ -475,20 +514,6 @@ }; /** - * Hide the image of the buildingRow to prevent strange effects when the row will be - * re-justified again - */ - JustifiedGallery.prototype.hideBuildingRowImages = function () { - for (var i = 0; i < this.buildingRow.entriesBuff.length; i++) { - if (this.settings.cssAnimation) { - this.buildingRow.entriesBuff[i].removeClass('entry-visible'); - } else { - this.buildingRow.entriesBuff[i].stop().fadeTo(0, 0); - } - } - }; - - /** * Update the entries searching it from the justified gallery HTML element * * @param norewind if norewind only the new entries will be changed (i.e. randomized, sorted or filtered) @@ -645,9 +670,9 @@ if ($entry.data('jg.createdCaption')) { // remove also the caption element (if created by jg) $entry.data('jg.createdCaption', undefined); - if ($caption != null) $caption.remove(); + if ($caption !== null) $caption.remove(); } else { - if ($caption != null) $caption.fadeTo(0, 1); + if ($caption !== null) $caption.fadeTo(0, 1); } }, this)); @@ -782,8 +807,8 @@ /* If we have the height and the width, we don't wait that the image is loaded, but we start directly * with the justification */ if (that.settings.waitThumbnailsLoad === false) { - var width = parseInt($image.attr('width'), 10); - var height = parseInt($image.attr('height'), 10); + var width = parseFloat($image.attr('width')); + var height = parseFloat($image.attr('height')); if (!isNaN(width) && !isNaN(height)) { $entry.data('jg.width', width); $entry.data('jg.height', height); @@ -798,9 +823,7 @@ imagesToLoad = true; // Spinner start - if (!that.isSpinnerActive()) { - that.startLoadingSpinnerAnimation(); - } + if (!that.isSpinnerActive()) that.startLoadingSpinnerAnimation(); that.onImageEvent(imageSrc, function (loadImg) { // image loaded $entry.data('jg.width', loadImg.width); @@ -814,8 +837,8 @@ } else { $entry.data('jg.loaded', true); - $entry.data('jg.width', $entry.width() | $entry.css('width') | 1); - $entry.data('jg.height', $entry.height() | $entry.css('height') | 1); + $entry.data('jg.width', $entry.width() | parseFloat($entry.css('width')) | 1); + $entry.data('jg.height', $entry.height() | parseFloat($entry.css('height')) | 1); } } @@ -883,15 +906,15 @@ if ($.type(this.settings.maxRowHeight) === 'string') { if (this.settings.maxRowHeight.match(/^[0-9]+%$/)) { - newMaxRowHeight.value = parseFloat(this.settings.maxRowHeight.match(/^([0-9])+%$/)[1]) / 100; - newMaxRowHeight.percentage = false; + newMaxRowHeight.value = parseFloat(this.settings.maxRowHeight.match(/^([0-9]+)%$/)[1]) / 100; + newMaxRowHeight.isPercentage = false; } else { newMaxRowHeight.value = parseFloat(this.settings.maxRowHeight); - newMaxRowHeight.percentage = true; + newMaxRowHeight.isPercentage = true; } } else if ($.type(this.settings.maxRowHeight) === 'number') { newMaxRowHeight.value = this.settings.maxRowHeight; - newMaxRowHeight.percentage = false; + newMaxRowHeight.isPercentage = false; } else { throw 'maxRowHeight must be a number or a percentage'; } @@ -900,7 +923,7 @@ if (isNaN(newMaxRowHeight.value)) throw 'invalid number for maxRowHeight'; // check values - if (newMaxRowHeight.percentage) { + if (newMaxRowHeight.isPercentage) { if (newMaxRowHeight.value < 100) newMaxRowHeight.value = 100; } else { if (newMaxRowHeight.value > 0 && newMaxRowHeight.value < this.settings.rowHeight) { @@ -922,10 +945,12 @@ this.checkOrConvertNumber(this.settings, 'margins'); this.checkOrConvertNumber(this.settings, 'border'); - if (this.settings.lastRow !== 'nojustify' && - this.settings.lastRow !== 'justify' && + if (this.settings.lastRow !== 'justify' && + this.settings.lastRow !== 'nojustify' && this.settings.lastRow !== 'left' && + this.settings.lastRow !== 'center' && + this.settings.lastRow !== 'right' && this.settings.lastRow !== 'hide') { - throw 'lastRow must be "nojustify", "justify" or "hide"'; + throw 'lastRow must be "justify", "nojustify", "left", "center", "right" or "hide"'; } this.checkOrConvertNumber(this.settings, 'justifyThreshold'); @@ -954,6 +979,7 @@ if ($.type(this.settings.fixedHeight) !== 'boolean') throw 'fixedHeight must be a boolean'; this.checkOrConvertNumber(this.settings, 'imagesAnimationDuration'); this.checkOrConvertNumber(this.settings, 'refreshTime'); + this.checkOrConvertNumber(this.settings, 'refreshSensitivity'); if ($.type(this.settings.randomize) !== 'boolean') throw 'randomize must be a boolean'; if ($.type(this.settings.selector) !== 'string') throw 'selector must be a string'; @@ -961,7 +987,7 @@ throw 'sort must be false or a comparison function'; } - if (this.settings.filter !== false && !$.isFunction(this.settings.sort) && + if (this.settings.filter !== false && !$.isFunction(this.settings.filter) && $.type(this.settings.filter) !== 'string') { throw 'filter must be false, a string or a filter function'; } @@ -1018,13 +1044,14 @@ if (typeof controller === 'undefined') { // Create controller and assign it to the object data if (typeof arg !== 'undefined' && arg !== null && $.type(arg) !== 'object') { + if (arg === 'destroy') return; // Just a call to an unexisting object throw 'The argument must be an object'; } controller = new JustifiedGallery($gallery, $.extend({}, $.fn.justifiedGallery.defaults, arg)); $gallery.data('jg.controller', controller); } else if (arg === 'norewind') { // In this case we don't rewind: we analyze only the latest images (e.g. to complete the last unfinished row - controller.hideBuildingRowImages(); + // ... left to be more readable } else if (arg === 'destroy') { controller.destroy(); return; @@ -1055,14 +1082,18 @@ 1024: '_b' // used as else case because it is the last } */ + thumbnailPath: undefined, /* If defined, sizeRangeSuffixes is not used, and this function is used to determine the + path relative to a specific thumbnail size. The function should accept respectively three arguments: + current path, width and height */ rowHeight: 120, - maxRowHeight: '200%', // negative value = no limits, number to express the value in pixels, - // '[0-9]+%' to express in percentage (e.g. 200% means that the row height - // can't exceed 2 * rowHeight) + maxRowHeight: -1, // negative value = no limits, number to express the value in pixels, + // '[0-9]+%' to express in percentage (e.g. 300% means that the row height + // can't exceed 3 * rowHeight) margins: 1, border: -1, // negative value = same as margins, 0 = disabled, any other value to set the border - lastRow: 'nojustify', // or can be 'justify' or 'hide' + lastRow: 'nojustify', // … which is the same as 'left', or can be 'justify', 'center', 'right' or 'hide' + justifyThreshold: 0.75, /* if row width / available space > 0.75 it will be always justified * (i.e. lastRow setting is not considered) */ fixedHeight: false, @@ -1078,7 +1109,8 @@ rel: null, // rewrite the rel of each analyzed links target: null, // rewrite the target of all links extension: /\.[^.\\/]+$/, // regexp to capture the extension of an image - refreshTime: 100, // time interval (in ms) to check if the page changes its width + refreshTime: 200, // time interval (in ms) to check if the page changes its width + refreshSensitivity: 0, // change in width allowed (in px) without re-building the gallery randomize: false, sort: false, /* - false: to do not sort diff --git a/library/justifiedGallery/jquery.justifiedGallery.min.js b/library/justifiedGallery/jquery.justifiedGallery.min.js index 74f333208..030636ccc 100644 --- a/library/justifiedGallery/jquery.justifiedGallery.min.js +++ b/library/justifiedGallery/jquery.justifiedGallery.min.js @@ -1,7 +1,7 @@ /*! - * Justified Gallery - v3.6.0 + * Justified Gallery - v3.6.1 * http://miromannino.github.io/Justified-Gallery/ * Copyright (c) 2015 Miro Mannino * Licensed under the MIT license. */ -!function(a){var b=function(b,c){this.settings=c,this.checkSettings(),this.imgAnalyzerTimeout=null,this.entries=null,this.buildingRow={entriesBuff:[],width:0,aspectRatio:0},this.lastAnalyzedIndex=-1,this.yield={every:2,flushed:0},this.border=c.border>=0?c.border:c.margins,this.maxRowHeight=this.retrieveMaxRowHeight(),this.suffixRanges=this.retrieveSuffixRanges(),this.offY=this.border,this.spinner={phase:0,timeSlot:150,$el:a('<div class="spinner"><span></span><span></span><span></span></div>'),intervalId:null},this.checkWidthIntervalId=null,this.galleryWidth=b.width(),this.$gallery=b};b.prototype.getSuffix=function(a,b){var c,d;for(c=a>b?a:b,d=0;d<this.suffixRanges.length;d++)if(c<=this.suffixRanges[d])return this.settings.sizeRangeSuffixes[this.suffixRanges[d]];return this.settings.sizeRangeSuffixes[this.suffixRanges[d-1]]},b.prototype.removeSuffix=function(a,b){return a.substring(0,a.length-b.length)},b.prototype.endsWith=function(a,b){return-1!==a.indexOf(b,a.length-b.length)},b.prototype.getUsedSuffix=function(a){for(var b in this.settings.sizeRangeSuffixes)if(this.settings.sizeRangeSuffixes.hasOwnProperty(b)){if(0===this.settings.sizeRangeSuffixes[b].length)continue;if(this.endsWith(a,this.settings.sizeRangeSuffixes[b]))return this.settings.sizeRangeSuffixes[b]}return""},b.prototype.newSrc=function(a,b,c){var d=a.match(this.settings.extension),e=null!=d?d[0]:"",f=a.replace(this.settings.extension,"");return f=this.removeSuffix(f,this.getUsedSuffix(f)),f+=this.getSuffix(b,c)+e},b.prototype.showImg=function(a,b){this.settings.cssAnimation?(a.addClass("entry-visible"),b&&b()):a.stop().fadeTo(this.settings.imagesAnimationDuration,1,b)},b.prototype.extractImgSrcFromImage=function(a){var b="undefined"!=typeof a.data("safe-src")?a.data("safe-src"):a.attr("src");return a.data("jg.originalSrc",b),b},b.prototype.imgFromEntry=function(a){var b=a.find("> img");return 0===b.length&&(b=a.find("> a > img")),0===b.length?null:b},b.prototype.captionFromEntry=function(a){var b=a.find("> .caption");return 0===b.length?null:b},b.prototype.displayEntry=function(b,c,d,e,f,g){b.width(e),b.height(g),b.css("top",d),b.css("left",c);var h=this.imgFromEntry(b);if(null!==h){h.css("width",e),h.css("height",f),h.css("margin-left",-e/2),h.css("margin-top",-f/2);var i=h.attr("src"),j=this.newSrc(i,e,f);h.one("error",function(){h.attr("src",h.data("jg.originalSrc"))});var k=function(){i!==j&&h.attr("src",j)};"skipped"===b.data("jg.loaded")?this.onImageEvent(i,a.proxy(function(){this.showImg(b,k),b.data("jg.loaded",!0)},this)):this.showImg(b,k)}else this.showImg(b);this.displayEntryCaption(b)},b.prototype.displayEntryCaption=function(b){var c=this.imgFromEntry(b);if(null!==c&&this.settings.captions){var d=this.captionFromEntry(b);if(null==d){var e=c.attr("alt");"undefined"==typeof e&&(e=b.attr("title")),"undefined"!=typeof e&&(d=a('<div class="caption">'+e+"</div>"),b.append(d),b.data("jg.createdCaption",!0))}null!==d&&(this.settings.cssAnimation||d.stop().fadeTo(0,this.settings.captionSettings.nonVisibleOpacity),this.addCaptionEventsHandlers(b))}else this.removeCaptionEventsHandlers(b)},b.prototype.onEntryMouseEnterForCaption=function(b){var c=this.captionFromEntry(a(b.currentTarget));this.settings.cssAnimation?c.addClass("caption-visible").removeClass("caption-hidden"):c.stop().fadeTo(this.settings.captionSettings.animationDuration,this.settings.captionSettings.visibleOpacity)},b.prototype.onEntryMouseLeaveForCaption=function(b){var c=this.captionFromEntry(a(b.currentTarget));this.settings.cssAnimation?c.removeClass("caption-visible").removeClass("caption-hidden"):c.stop().fadeTo(this.settings.captionSettings.animationDuration,this.settings.captionSettings.nonVisibleOpacity)},b.prototype.addCaptionEventsHandlers=function(b){var c=b.data("jg.captionMouseEvents");"undefined"==typeof c&&(c={mouseenter:a.proxy(this.onEntryMouseEnterForCaption,this),mouseleave:a.proxy(this.onEntryMouseLeaveForCaption,this)},b.on("mouseenter",void 0,void 0,c.mouseenter),b.on("mouseleave",void 0,void 0,c.mouseleave),b.data("jg.captionMouseEvents",c))},b.prototype.removeCaptionEventsHandlers=function(a){var b=a.data("jg.captionMouseEvents");"undefined"!=typeof b&&(a.off("mouseenter",void 0,b.mouseenter),a.off("mouseleave",void 0,b.mouseleave),a.removeData("jg.captionMouseEvents"))},b.prototype.prepareBuildingRow=function(a){var b,c,d,e,f,g=!0,h=0,i=this.galleryWidth-2*this.border-(this.buildingRow.entriesBuff.length-1)*this.settings.margins,j=i/this.buildingRow.aspectRatio,k=this.buildingRow.width/i>this.settings.justifyThreshold;if(a&&"hide"===this.settings.lastRow&&!k){for(b=0;b<this.buildingRow.entriesBuff.length;b++)c=this.buildingRow.entriesBuff[b],this.settings.cssAnimation?c.removeClass("entry-visible"):c.stop().fadeTo(0,0);return-1}for(a&&!k&&"nojustify"===this.settings.lastRow&&(g=!1),b=0;b<this.buildingRow.entriesBuff.length;b++)c=this.buildingRow.entriesBuff[b],d=c.data("jg.width")/c.data("jg.height"),g?(e=b===this.buildingRow.entriesBuff.length-1?i:j*d,f=j):(e=this.settings.rowHeight*d,f=this.settings.rowHeight),i-=Math.round(e),c.data("jg.jwidth",Math.round(e)),c.data("jg.jheight",Math.ceil(f)),(0===b||h>f)&&(h=f);return this.settings.fixedHeight&&h>this.settings.rowHeight&&(h=this.settings.rowHeight),{minHeight:h,justify:g}},b.prototype.clearBuildingRow=function(){this.buildingRow.entriesBuff=[],this.buildingRow.aspectRatio=0,this.buildingRow.width=0},b.prototype.flushRow=function(a){var b,c,d,e=this.settings,f=this.border;if(d=this.prepareBuildingRow(a),c=d.minHeight,a&&"hide"===e.lastRow&&-1===c)return void this.clearBuildingRow();this.maxRowHeight.percentage?this.maxRowHeight.value*e.rowHeight<c&&(c=this.maxRowHeight.value*e.rowHeight):this.maxRowHeight.value>0&&this.maxRowHeight.value<c&&(c=this.maxRowHeight.value);for(var g=0;g<this.buildingRow.entriesBuff.length;g++)b=this.buildingRow.entriesBuff[g],this.displayEntry(b,f,this.offY,b.data("jg.jwidth"),b.data("jg.jheight"),c),f+=b.data("jg.jwidth")+e.margins;this.$gallery.height(this.offY+c+this.border+(this.isSpinnerActive()?this.getSpinnerHeight():0)),(!a||c<=this.settings.rowHeight&&d.justify)&&(this.offY+=c+this.settings.margins,this.clearBuildingRow(),this.$gallery.trigger("jg.rowflush"))},b.prototype.checkWidth=function(){this.checkWidthIntervalId=setInterval(a.proxy(function(){var a=parseInt(this.$gallery.width(),10);this.galleryWidth!==a&&(this.galleryWidth=a,this.rewind(),this.startImgAnalyzer(!0))},this),this.settings.refreshTime)},b.prototype.isSpinnerActive=function(){return null!=this.spinner.intervalId},b.prototype.getSpinnerHeight=function(){return this.spinner.$el.innerHeight()},b.prototype.stopLoadingSpinnerAnimation=function(){clearInterval(this.spinner.intervalId),this.spinner.intervalId=null,this.$gallery.height(this.$gallery.height()-this.getSpinnerHeight()),this.spinner.$el.detach()},b.prototype.startLoadingSpinnerAnimation=function(){var a=this.spinner,b=a.$el.find("span");clearInterval(a.intervalId),this.$gallery.append(a.$el),this.$gallery.height(this.offY+this.getSpinnerHeight()),a.intervalId=setInterval(function(){a.phase<b.length?b.eq(a.phase).fadeTo(a.timeSlot,1):b.eq(a.phase-b.length).fadeTo(a.timeSlot,0),a.phase=(a.phase+1)%(2*b.length)},a.timeSlot)},b.prototype.rewind=function(){this.lastAnalyzedIndex=-1,this.offY=this.border,this.clearBuildingRow()},b.prototype.hideBuildingRowImages=function(){for(var a=0;a<this.buildingRow.entriesBuff.length;a++)this.settings.cssAnimation?this.buildingRow.entriesBuff[a].removeClass("entry-visible"):this.buildingRow.entriesBuff[a].stop().fadeTo(0,0)},b.prototype.updateEntries=function(b){return this.entries=this.$gallery.find(this.settings.selector).toArray(),0===this.entries.length?!1:(this.settings.filter?this.modifyEntries(this.filterArray,b):this.modifyEntries(this.resetFilters,b),a.isFunction(this.settings.sort)?this.modifyEntries(this.sortArray,b):this.settings.randomize&&this.modifyEntries(this.shuffleArray,b),!0)},b.prototype.insertToGallery=function(b){var c=this;a.each(b,function(){a(this).appendTo(c.$gallery)})},b.prototype.shuffleArray=function(a){var b,c,d;for(b=a.length-1;b>0;b--)c=Math.floor(Math.random()*(b+1)),d=a[b],a[b]=a[c],a[c]=d;return this.insertToGallery(a),a},b.prototype.sortArray=function(a){return a.sort(this.settings.sort),this.insertToGallery(a),a},b.prototype.resetFilters=function(b){for(var c=0;c<b.length;c++)a(b[c]).removeClass("jg-filtered");return b},b.prototype.filterArray=function(b){var c=this.settings;return"string"===a.type(c.filter)?b.filter(function(b){var d=a(b);return d.is(c.filter)?(d.removeClass("jg-filtered"),!0):(d.addClass("jg-filtered"),!1)}):a.isFunction(c.filter)?b.filter(c.filter):void 0},b.prototype.modifyEntries=function(a,b){var c=b?this.entries.splice(this.lastAnalyzedIndex+1,this.entries.length-this.lastAnalyzedIndex-1):this.entries;c=a.call(this,c),this.entries=b?this.entries.concat(c):c},b.prototype.destroy=function(){clearInterval(this.checkWidthIntervalId),a.each(this.entries,a.proxy(function(b,c){var d=a(c);d.css("width",""),d.css("height",""),d.css("top",""),d.css("left",""),d.data("jg.loaded",void 0),d.removeClass("jg-entry");var e=this.imgFromEntry(d);e.css("width",""),e.css("height",""),e.css("margin-left",""),e.css("margin-top",""),e.attr("src",e.data("jg.originalSrc")),e.data("jg.originalSrc",void 0),this.removeCaptionEventsHandlers(d);var f=this.captionFromEntry(d);d.data("jg.createdCaption")?(d.data("jg.createdCaption",void 0),null!=f&&f.remove()):null!=f&&f.fadeTo(0,1)},this)),this.$gallery.css("height",""),this.$gallery.removeClass("justified-gallery"),this.$gallery.data("jg.controller",void 0)},b.prototype.analyzeImages=function(b){for(var c=this.lastAnalyzedIndex+1;c<this.entries.length;c++){var d=a(this.entries[c]);if(d.data("jg.loaded")===!0||"skipped"===d.data("jg.loaded")){var e=this.galleryWidth-2*this.border-(this.buildingRow.entriesBuff.length-1)*this.settings.margins,f=d.data("jg.width")/d.data("jg.height");if(e/(this.buildingRow.aspectRatio+f)<this.settings.rowHeight&&(this.flushRow(!1),++this.yield.flushed>=this.yield.every))return void this.startImgAnalyzer(b);this.buildingRow.entriesBuff.push(d),this.buildingRow.aspectRatio+=f,this.buildingRow.width+=f*this.settings.rowHeight,this.lastAnalyzedIndex=c}else if("error"!==d.data("jg.loaded"))return}this.buildingRow.entriesBuff.length>0&&this.flushRow(!0),this.isSpinnerActive()&&this.stopLoadingSpinnerAnimation(),this.stopImgAnalyzerStarter(),this.$gallery.trigger(b?"jg.resize":"jg.complete")},b.prototype.stopImgAnalyzerStarter=function(){this.yield.flushed=0,null!==this.imgAnalyzerTimeout&&clearTimeout(this.imgAnalyzerTimeout)},b.prototype.startImgAnalyzer=function(a){var b=this;this.stopImgAnalyzerStarter(),this.imgAnalyzerTimeout=setTimeout(function(){b.analyzeImages(a)},.001)},b.prototype.onImageEvent=function(b,c,d){if(c||d){var e=new Image,f=a(e);c&&f.one("load",function(){f.off("load error"),c(e)}),d&&f.one("error",function(){f.off("load error"),d(e)}),e.src=b}},b.prototype.init=function(){var b=!1,c=!1,d=this;a.each(this.entries,function(e,f){var g=a(f),h=d.imgFromEntry(g);if(g.addClass("jg-entry"),g.data("jg.loaded")!==!0&&"skipped"!==g.data("jg.loaded"))if(null!==d.settings.rel&&g.attr("rel",d.settings.rel),null!==d.settings.target&&g.attr("target",d.settings.target),null!==h){var i=d.extractImgSrcFromImage(h);if(h.attr("src",i),d.settings.waitThumbnailsLoad===!1){var j=parseInt(h.attr("width"),10),k=parseInt(h.attr("height"),10);if(!isNaN(j)&&!isNaN(k))return g.data("jg.width",j),g.data("jg.height",k),g.data("jg.loaded","skipped"),c=!0,d.startImgAnalyzer(!1),!0}g.data("jg.loaded",!1),b=!0,d.isSpinnerActive()||d.startLoadingSpinnerAnimation(),d.onImageEvent(i,function(a){g.data("jg.width",a.width),g.data("jg.height",a.height),g.data("jg.loaded",!0),d.startImgAnalyzer(!1)},function(){g.data("jg.loaded","error"),d.startImgAnalyzer(!1)})}else g.data("jg.loaded",!0),g.data("jg.width",g.width()|g.css("width")|1),g.data("jg.height",g.height()|g.css("height")|1)}),b||c||this.startImgAnalyzer(!1),this.checkWidth()},b.prototype.checkOrConvertNumber=function(b,c){if("string"===a.type(b[c])&&(b[c]=parseFloat(b[c])),"number"!==a.type(b[c]))throw c+" must be a number";if(isNaN(b[c]))throw"invalid number for "+c},b.prototype.checkSizeRangesSuffixes=function(){if("object"!==a.type(this.settings.sizeRangeSuffixes))throw"sizeRangeSuffixes must be defined and must be an object";var b=[];for(var c in this.settings.sizeRangeSuffixes)this.settings.sizeRangeSuffixes.hasOwnProperty(c)&&b.push(c);for(var d={0:""},e=0;e<b.length;e++)if("string"===a.type(b[e]))try{var f=parseInt(b[e].replace(/^[a-z]+/,""),10);d[f]=this.settings.sizeRangeSuffixes[b[e]]}catch(g){throw"sizeRangeSuffixes keys must contains correct numbers ("+g+")"}else d[b[e]]=this.settings.sizeRangeSuffixes[b[e]];this.settings.sizeRangeSuffixes=d},b.prototype.retrieveMaxRowHeight=function(){var b={};if("string"===a.type(this.settings.maxRowHeight))this.settings.maxRowHeight.match(/^[0-9]+%$/)?(b.value=parseFloat(this.settings.maxRowHeight.match(/^([0-9])+%$/)[1])/100,b.percentage=!1):(b.value=parseFloat(this.settings.maxRowHeight),b.percentage=!0);else{if("number"!==a.type(this.settings.maxRowHeight))throw"maxRowHeight must be a number or a percentage";b.value=this.settings.maxRowHeight,b.percentage=!1}if(isNaN(b.value))throw"invalid number for maxRowHeight";return b.percentage?b.value<100&&(b.value=100):b.value>0&&b.value<this.settings.rowHeight&&(b.value=this.settings.rowHeight),b},b.prototype.checkSettings=function(){if(this.checkSizeRangesSuffixes(),this.checkOrConvertNumber(this.settings,"rowHeight"),this.checkOrConvertNumber(this.settings,"margins"),this.checkOrConvertNumber(this.settings,"border"),"nojustify"!==this.settings.lastRow&&"justify"!==this.settings.lastRow&&"hide"!==this.settings.lastRow)throw'lastRow must be "nojustify", "justify" or "hide"';if(this.checkOrConvertNumber(this.settings,"justifyThreshold"),this.settings.justifyThreshold<0||this.settings.justifyThreshold>1)throw"justifyThreshold must be in the interval [0,1]";if("boolean"!==a.type(this.settings.cssAnimation))throw"cssAnimation must be a boolean";if("boolean"!==a.type(this.settings.captions))throw"captions must be a boolean";if(this.checkOrConvertNumber(this.settings.captionSettings,"animationDuration"),this.checkOrConvertNumber(this.settings.captionSettings,"visibleOpacity"),this.settings.captionSettings.visibleOpacity<0||this.settings.captionSettings.visibleOpacity>1)throw"captionSettings.visibleOpacity must be in the interval [0, 1]";if(this.checkOrConvertNumber(this.settings.captionSettings,"nonVisibleOpacity"),this.settings.captionSettings.nonVisibleOpacity<0||this.settings.captionSettings.nonVisibleOpacity>1)throw"captionSettings.nonVisibleOpacity must be in the interval [0, 1]";if("boolean"!==a.type(this.settings.fixedHeight))throw"fixedHeight must be a boolean";if(this.checkOrConvertNumber(this.settings,"imagesAnimationDuration"),this.checkOrConvertNumber(this.settings,"refreshTime"),"boolean"!==a.type(this.settings.randomize))throw"randomize must be a boolean";if("string"!==a.type(this.settings.selector))throw"selector must be a string";if(this.settings.sort!==!1&&!a.isFunction(this.settings.sort))throw"sort must be false or a comparison function";if(this.settings.filter!==!1&&!a.isFunction(this.settings.sort)&&"string"!==a.type(this.settings.filter))throw"filter must be false, a string or a filter function"},b.prototype.retrieveSuffixRanges=function(){var a=[];for(var b in this.settings.sizeRangeSuffixes)this.settings.sizeRangeSuffixes.hasOwnProperty(b)&&a.push(parseInt(b,10));return a.sort(function(a,b){return a>b?1:b>a?-1:0}),a},b.prototype.updateSettings=function(b){this.settings=a.extend({},this.settings,b),this.checkSettings(),this.border=this.settings.border>=0?this.settings.border:this.settings.margins,this.maxRowHeight=this.retrieveMaxRowHeight(),this.suffixRanges=this.retrieveSuffixRanges()},a.fn.justifiedGallery=function(c){return this.each(function(d,e){var f=a(e);f.addClass("justified-gallery");var g=f.data("jg.controller");if("undefined"==typeof g){if("undefined"!=typeof c&&null!==c&&"object"!==a.type(c))throw"The argument must be an object";g=new b(f,a.extend({},a.fn.justifiedGallery.defaults,c)),f.data("jg.controller",g)}else if("norewind"===c)g.hideBuildingRowImages();else{if("destroy"===c)return void g.destroy();g.updateSettings(c),g.rewind()}g.updateEntries("norewind"===c)&&g.init()})},a.fn.justifiedGallery.defaults={sizeRangeSuffixes:{},rowHeight:120,maxRowHeight:"200%",margins:1,border:-1,lastRow:"nojustify",justifyThreshold:.75,fixedHeight:!1,waitThumbnailsLoad:!0,captions:!0,cssAnimation:!1,imagesAnimationDuration:500,captionSettings:{animationDuration:500,visibleOpacity:.7,nonVisibleOpacity:0},rel:null,target:null,extension:/\.[^.\\/]+$/,refreshTime:100,randomize:!1,sort:!1,filter:!1,selector:"> a, > div:not(.spinner)"}}(jQuery);
\ No newline at end of file +!function(a){var b=function(b,c){this.settings=c,this.checkSettings(),this.imgAnalyzerTimeout=null,this.entries=null,this.buildingRow={entriesBuff:[],width:0,height:0,aspectRatio:0},this.lastAnalyzedIndex=-1,this.yield={every:2,flushed:0},this.border=c.border>=0?c.border:c.margins,this.maxRowHeight=this.retrieveMaxRowHeight(),this.suffixRanges=this.retrieveSuffixRanges(),this.offY=this.border,this.spinner={phase:0,timeSlot:150,$el:a('<div class="spinner"><span></span><span></span><span></span></div>'),intervalId:null},this.checkWidthIntervalId=null,this.galleryWidth=b.width(),this.$gallery=b};b.prototype.getSuffix=function(a,b){var c,d;for(c=a>b?a:b,d=0;d<this.suffixRanges.length;d++)if(c<=this.suffixRanges[d])return this.settings.sizeRangeSuffixes[this.suffixRanges[d]];return this.settings.sizeRangeSuffixes[this.suffixRanges[d-1]]},b.prototype.removeSuffix=function(a,b){return a.substring(0,a.length-b.length)},b.prototype.endsWith=function(a,b){return-1!==a.indexOf(b,a.length-b.length)},b.prototype.getUsedSuffix=function(a){for(var b in this.settings.sizeRangeSuffixes)if(this.settings.sizeRangeSuffixes.hasOwnProperty(b)){if(0===this.settings.sizeRangeSuffixes[b].length)continue;if(this.endsWith(a,this.settings.sizeRangeSuffixes[b]))return this.settings.sizeRangeSuffixes[b]}return""},b.prototype.newSrc=function(a,b,c){var d;if(this.settings.thumbnailPath)d=this.settings.thumbnailPath(a,b,c);else{var e=a.match(this.settings.extension),f=null!==e?e[0]:"";d=a.replace(this.settings.extension,""),d=this.removeSuffix(d,this.getUsedSuffix(d)),d+=this.getSuffix(b,c)+f}return d},b.prototype.showImg=function(a,b){this.settings.cssAnimation?(a.addClass("entry-visible"),b&&b()):a.stop().fadeTo(this.settings.imagesAnimationDuration,1,b)},b.prototype.extractImgSrcFromImage=function(a){var b="undefined"!=typeof a.data("safe-src")?a.data("safe-src"):a.attr("src");return a.data("jg.originalSrc",b),b},b.prototype.imgFromEntry=function(a){var b=a.find("> img");return 0===b.length&&(b=a.find("> a > img")),0===b.length?null:b},b.prototype.captionFromEntry=function(a){var b=a.find("> .caption");return 0===b.length?null:b},b.prototype.displayEntry=function(b,c,d,e,f,g){b.width(e),b.height(g),b.css("top",d),b.css("left",c);var h=this.imgFromEntry(b);if(null!==h){h.css("width",e),h.css("height",f),h.css("margin-left",-e/2),h.css("margin-top",-f/2);var i=h.attr("src"),j=this.newSrc(i,e,f);h.one("error",function(){h.attr("src",h.data("jg.originalSrc"))});var k=function(){i!==j&&h.attr("src",j)};"skipped"===b.data("jg.loaded")?this.onImageEvent(i,a.proxy(function(){this.showImg(b,k),b.data("jg.loaded",!0)},this)):this.showImg(b,k)}else this.showImg(b);this.displayEntryCaption(b)},b.prototype.displayEntryCaption=function(b){var c=this.imgFromEntry(b);if(null!==c&&this.settings.captions){var d=this.captionFromEntry(b);if(null===d){var e=c.attr("alt");this.isValidCaption(e)||(e=b.attr("title")),this.isValidCaption(e)&&(d=a('<div class="caption">'+e+"</div>"),b.append(d),b.data("jg.createdCaption",!0))}null!==d&&(this.settings.cssAnimation||d.stop().fadeTo(0,this.settings.captionSettings.nonVisibleOpacity),this.addCaptionEventsHandlers(b))}else this.removeCaptionEventsHandlers(b)},b.prototype.isValidCaption=function(a){return"undefined"!=typeof a&&a.length>0},b.prototype.onEntryMouseEnterForCaption=function(b){var c=this.captionFromEntry(a(b.currentTarget));this.settings.cssAnimation?c.addClass("caption-visible").removeClass("caption-hidden"):c.stop().fadeTo(this.settings.captionSettings.animationDuration,this.settings.captionSettings.visibleOpacity)},b.prototype.onEntryMouseLeaveForCaption=function(b){var c=this.captionFromEntry(a(b.currentTarget));this.settings.cssAnimation?c.removeClass("caption-visible").removeClass("caption-hidden"):c.stop().fadeTo(this.settings.captionSettings.animationDuration,this.settings.captionSettings.nonVisibleOpacity)},b.prototype.addCaptionEventsHandlers=function(b){var c=b.data("jg.captionMouseEvents");"undefined"==typeof c&&(c={mouseenter:a.proxy(this.onEntryMouseEnterForCaption,this),mouseleave:a.proxy(this.onEntryMouseLeaveForCaption,this)},b.on("mouseenter",void 0,void 0,c.mouseenter),b.on("mouseleave",void 0,void 0,c.mouseleave),b.data("jg.captionMouseEvents",c))},b.prototype.removeCaptionEventsHandlers=function(a){var b=a.data("jg.captionMouseEvents");"undefined"!=typeof b&&(a.off("mouseenter",void 0,b.mouseenter),a.off("mouseleave",void 0,b.mouseleave),a.removeData("jg.captionMouseEvents"))},b.prototype.prepareBuildingRow=function(a){var b,c,d,e,f,g=!0,h=0,i=this.galleryWidth-2*this.border-(this.buildingRow.entriesBuff.length-1)*this.settings.margins,j=i/this.buildingRow.aspectRatio,k=this.buildingRow.width/i>this.settings.justifyThreshold;if(a&&"hide"===this.settings.lastRow&&!k){for(b=0;b<this.buildingRow.entriesBuff.length;b++)c=this.buildingRow.entriesBuff[b],this.settings.cssAnimation?c.removeClass("entry-visible"):c.stop().fadeTo(0,0);return-1}for(a&&!k&&"justify"!==this.settings.lastRow&&"hide"!==this.settings.lastRow&&(g=!1),b=0;b<this.buildingRow.entriesBuff.length;b++)c=this.buildingRow.entriesBuff[b],d=c.data("jg.width")/c.data("jg.height"),g?(e=b===this.buildingRow.entriesBuff.length-1?i:j*d,f=j):(e=this.settings.rowHeight*d,f=this.settings.rowHeight),i-=Math.round(e),c.data("jg.jwidth",Math.round(e)),c.data("jg.jheight",Math.ceil(f)),(0===b||h>f)&&(h=f);return this.settings.fixedHeight&&h>this.settings.rowHeight&&(h=this.settings.rowHeight),this.buildingRow.height=h,g},b.prototype.clearBuildingRow=function(){this.buildingRow.entriesBuff=[],this.buildingRow.aspectRatio=0,this.buildingRow.width=0},b.prototype.flushRow=function(a){var b,c,d,e=this.settings,f=this.border;if(c=this.prepareBuildingRow(a),a&&"hide"===e.lastRow&&-1===this.buildingRow.height)return void this.clearBuildingRow();if(this.maxRowHeight.isPercentage?this.maxRowHeight.value*e.rowHeight<this.buildingRow.height&&(this.buildingRow.height=this.maxRowHeight.value*e.rowHeight):this.maxRowHeight.value>0&&this.maxRowHeight.value<this.buildingRow.height&&(this.buildingRow.height=this.maxRowHeight.value),"center"===e.lastRow||"right"===e.lastRow){var g=this.galleryWidth-2*this.border-(this.buildingRow.entriesBuff.length-1)*e.margins;for(d=0;d<this.buildingRow.entriesBuff.length;d++)b=this.buildingRow.entriesBuff[d],g-=b.data("jg.jwidth");"center"===e.lastRow?f+=g/2:"right"===e.lastRow&&(f+=g)}for(d=0;d<this.buildingRow.entriesBuff.length;d++)b=this.buildingRow.entriesBuff[d],this.displayEntry(b,f,this.offY,b.data("jg.jwidth"),b.data("jg.jheight"),this.buildingRow.height),f+=b.data("jg.jwidth")+e.margins;this.$gallery.height(this.offY+this.buildingRow.height+this.border+(this.isSpinnerActive()?this.getSpinnerHeight():0)),(!a||this.buildingRow.height<=e.rowHeight&&c)&&(this.offY+=this.buildingRow.height+e.margins,this.clearBuildingRow(),this.$gallery.trigger("jg.rowflush"))},b.prototype.checkWidth=function(){this.checkWidthIntervalId=setInterval(a.proxy(function(){var a=parseFloat(this.$gallery.width());Math.abs(a-this.galleryWidth)>this.settings.refreshSensitivity&&(this.galleryWidth=a,this.rewind(),this.startImgAnalyzer(!0))},this),this.settings.refreshTime)},b.prototype.isSpinnerActive=function(){return null!==this.spinner.intervalId},b.prototype.getSpinnerHeight=function(){return this.spinner.$el.innerHeight()},b.prototype.stopLoadingSpinnerAnimation=function(){clearInterval(this.spinner.intervalId),this.spinner.intervalId=null,this.$gallery.height(this.$gallery.height()-this.getSpinnerHeight()),this.spinner.$el.detach()},b.prototype.startLoadingSpinnerAnimation=function(){var a=this.spinner,b=a.$el.find("span");clearInterval(a.intervalId),this.$gallery.append(a.$el),this.$gallery.height(this.offY+this.buildingRow.height+this.getSpinnerHeight()),a.intervalId=setInterval(function(){a.phase<b.length?b.eq(a.phase).fadeTo(a.timeSlot,1):b.eq(a.phase-b.length).fadeTo(a.timeSlot,0),a.phase=(a.phase+1)%(2*b.length)},a.timeSlot)},b.prototype.rewind=function(){this.lastAnalyzedIndex=-1,this.offY=this.border,this.clearBuildingRow()},b.prototype.updateEntries=function(b){return this.entries=this.$gallery.find(this.settings.selector).toArray(),0===this.entries.length?!1:(this.settings.filter?this.modifyEntries(this.filterArray,b):this.modifyEntries(this.resetFilters,b),a.isFunction(this.settings.sort)?this.modifyEntries(this.sortArray,b):this.settings.randomize&&this.modifyEntries(this.shuffleArray,b),!0)},b.prototype.insertToGallery=function(b){var c=this;a.each(b,function(){a(this).appendTo(c.$gallery)})},b.prototype.shuffleArray=function(a){var b,c,d;for(b=a.length-1;b>0;b--)c=Math.floor(Math.random()*(b+1)),d=a[b],a[b]=a[c],a[c]=d;return this.insertToGallery(a),a},b.prototype.sortArray=function(a){return a.sort(this.settings.sort),this.insertToGallery(a),a},b.prototype.resetFilters=function(b){for(var c=0;c<b.length;c++)a(b[c]).removeClass("jg-filtered");return b},b.prototype.filterArray=function(b){var c=this.settings;return"string"===a.type(c.filter)?b.filter(function(b){var d=a(b);return d.is(c.filter)?(d.removeClass("jg-filtered"),!0):(d.addClass("jg-filtered"),!1)}):a.isFunction(c.filter)?b.filter(c.filter):void 0},b.prototype.modifyEntries=function(a,b){var c=b?this.entries.splice(this.lastAnalyzedIndex+1,this.entries.length-this.lastAnalyzedIndex-1):this.entries;c=a.call(this,c),this.entries=b?this.entries.concat(c):c},b.prototype.destroy=function(){clearInterval(this.checkWidthIntervalId),a.each(this.entries,a.proxy(function(b,c){var d=a(c);d.css("width",""),d.css("height",""),d.css("top",""),d.css("left",""),d.data("jg.loaded",void 0),d.removeClass("jg-entry");var e=this.imgFromEntry(d);e.css("width",""),e.css("height",""),e.css("margin-left",""),e.css("margin-top",""),e.attr("src",e.data("jg.originalSrc")),e.data("jg.originalSrc",void 0),this.removeCaptionEventsHandlers(d);var f=this.captionFromEntry(d);d.data("jg.createdCaption")?(d.data("jg.createdCaption",void 0),null!==f&&f.remove()):null!==f&&f.fadeTo(0,1)},this)),this.$gallery.css("height",""),this.$gallery.removeClass("justified-gallery"),this.$gallery.data("jg.controller",void 0)},b.prototype.analyzeImages=function(b){for(var c=this.lastAnalyzedIndex+1;c<this.entries.length;c++){var d=a(this.entries[c]);if(d.data("jg.loaded")===!0||"skipped"===d.data("jg.loaded")){var e=this.galleryWidth-2*this.border-(this.buildingRow.entriesBuff.length-1)*this.settings.margins,f=d.data("jg.width")/d.data("jg.height");if(e/(this.buildingRow.aspectRatio+f)<this.settings.rowHeight&&(this.flushRow(!1),++this.yield.flushed>=this.yield.every))return void this.startImgAnalyzer(b);this.buildingRow.entriesBuff.push(d),this.buildingRow.aspectRatio+=f,this.buildingRow.width+=f*this.settings.rowHeight,this.lastAnalyzedIndex=c}else if("error"!==d.data("jg.loaded"))return}this.buildingRow.entriesBuff.length>0&&this.flushRow(!0),this.isSpinnerActive()&&this.stopLoadingSpinnerAnimation(),this.stopImgAnalyzerStarter(),this.$gallery.trigger(b?"jg.resize":"jg.complete")},b.prototype.stopImgAnalyzerStarter=function(){this.yield.flushed=0,null!==this.imgAnalyzerTimeout&&clearTimeout(this.imgAnalyzerTimeout)},b.prototype.startImgAnalyzer=function(a){var b=this;this.stopImgAnalyzerStarter(),this.imgAnalyzerTimeout=setTimeout(function(){b.analyzeImages(a)},.001)},b.prototype.onImageEvent=function(b,c,d){if(c||d){var e=new Image,f=a(e);c&&f.one("load",function(){f.off("load error"),c(e)}),d&&f.one("error",function(){f.off("load error"),d(e)}),e.src=b}},b.prototype.init=function(){var b=!1,c=!1,d=this;a.each(this.entries,function(e,f){var g=a(f),h=d.imgFromEntry(g);if(g.addClass("jg-entry"),g.data("jg.loaded")!==!0&&"skipped"!==g.data("jg.loaded"))if(null!==d.settings.rel&&g.attr("rel",d.settings.rel),null!==d.settings.target&&g.attr("target",d.settings.target),null!==h){var i=d.extractImgSrcFromImage(h);if(h.attr("src",i),d.settings.waitThumbnailsLoad===!1){var j=parseFloat(h.attr("width")),k=parseFloat(h.attr("height"));if(!isNaN(j)&&!isNaN(k))return g.data("jg.width",j),g.data("jg.height",k),g.data("jg.loaded","skipped"),c=!0,d.startImgAnalyzer(!1),!0}g.data("jg.loaded",!1),b=!0,d.isSpinnerActive()||d.startLoadingSpinnerAnimation(),d.onImageEvent(i,function(a){g.data("jg.width",a.width),g.data("jg.height",a.height),g.data("jg.loaded",!0),d.startImgAnalyzer(!1)},function(){g.data("jg.loaded","error"),d.startImgAnalyzer(!1)})}else g.data("jg.loaded",!0),g.data("jg.width",g.width()|parseFloat(g.css("width"))|1),g.data("jg.height",g.height()|parseFloat(g.css("height"))|1)}),b||c||this.startImgAnalyzer(!1),this.checkWidth()},b.prototype.checkOrConvertNumber=function(b,c){if("string"===a.type(b[c])&&(b[c]=parseFloat(b[c])),"number"!==a.type(b[c]))throw c+" must be a number";if(isNaN(b[c]))throw"invalid number for "+c},b.prototype.checkSizeRangesSuffixes=function(){if("object"!==a.type(this.settings.sizeRangeSuffixes))throw"sizeRangeSuffixes must be defined and must be an object";var b=[];for(var c in this.settings.sizeRangeSuffixes)this.settings.sizeRangeSuffixes.hasOwnProperty(c)&&b.push(c);for(var d={0:""},e=0;e<b.length;e++)if("string"===a.type(b[e]))try{var f=parseInt(b[e].replace(/^[a-z]+/,""),10);d[f]=this.settings.sizeRangeSuffixes[b[e]]}catch(g){throw"sizeRangeSuffixes keys must contains correct numbers ("+g+")"}else d[b[e]]=this.settings.sizeRangeSuffixes[b[e]];this.settings.sizeRangeSuffixes=d},b.prototype.retrieveMaxRowHeight=function(){var b={};if("string"===a.type(this.settings.maxRowHeight))this.settings.maxRowHeight.match(/^[0-9]+%$/)?(b.value=parseFloat(this.settings.maxRowHeight.match(/^([0-9]+)%$/)[1])/100,b.isPercentage=!1):(b.value=parseFloat(this.settings.maxRowHeight),b.isPercentage=!0);else{if("number"!==a.type(this.settings.maxRowHeight))throw"maxRowHeight must be a number or a percentage";b.value=this.settings.maxRowHeight,b.isPercentage=!1}if(isNaN(b.value))throw"invalid number for maxRowHeight";return b.isPercentage?b.value<100&&(b.value=100):b.value>0&&b.value<this.settings.rowHeight&&(b.value=this.settings.rowHeight),b},b.prototype.checkSettings=function(){if(this.checkSizeRangesSuffixes(),this.checkOrConvertNumber(this.settings,"rowHeight"),this.checkOrConvertNumber(this.settings,"margins"),this.checkOrConvertNumber(this.settings,"border"),"justify"!==this.settings.lastRow&&"nojustify"!==this.settings.lastRow&&"left"!==this.settings.lastRow&&"center"!==this.settings.lastRow&&"right"!==this.settings.lastRow&&"hide"!==this.settings.lastRow)throw'lastRow must be "justify", "nojustify", "left", "center", "right" or "hide"';if(this.checkOrConvertNumber(this.settings,"justifyThreshold"),this.settings.justifyThreshold<0||this.settings.justifyThreshold>1)throw"justifyThreshold must be in the interval [0,1]";if("boolean"!==a.type(this.settings.cssAnimation))throw"cssAnimation must be a boolean";if("boolean"!==a.type(this.settings.captions))throw"captions must be a boolean";if(this.checkOrConvertNumber(this.settings.captionSettings,"animationDuration"),this.checkOrConvertNumber(this.settings.captionSettings,"visibleOpacity"),this.settings.captionSettings.visibleOpacity<0||this.settings.captionSettings.visibleOpacity>1)throw"captionSettings.visibleOpacity must be in the interval [0, 1]";if(this.checkOrConvertNumber(this.settings.captionSettings,"nonVisibleOpacity"),this.settings.captionSettings.nonVisibleOpacity<0||this.settings.captionSettings.nonVisibleOpacity>1)throw"captionSettings.nonVisibleOpacity must be in the interval [0, 1]";if("boolean"!==a.type(this.settings.fixedHeight))throw"fixedHeight must be a boolean";if(this.checkOrConvertNumber(this.settings,"imagesAnimationDuration"),this.checkOrConvertNumber(this.settings,"refreshTime"),this.checkOrConvertNumber(this.settings,"refreshSensitivity"),"boolean"!==a.type(this.settings.randomize))throw"randomize must be a boolean";if("string"!==a.type(this.settings.selector))throw"selector must be a string";if(this.settings.sort!==!1&&!a.isFunction(this.settings.sort))throw"sort must be false or a comparison function";if(this.settings.filter!==!1&&!a.isFunction(this.settings.filter)&&"string"!==a.type(this.settings.filter))throw"filter must be false, a string or a filter function"},b.prototype.retrieveSuffixRanges=function(){var a=[];for(var b in this.settings.sizeRangeSuffixes)this.settings.sizeRangeSuffixes.hasOwnProperty(b)&&a.push(parseInt(b,10));return a.sort(function(a,b){return a>b?1:b>a?-1:0}),a},b.prototype.updateSettings=function(b){this.settings=a.extend({},this.settings,b),this.checkSettings(),this.border=this.settings.border>=0?this.settings.border:this.settings.margins,this.maxRowHeight=this.retrieveMaxRowHeight(),this.suffixRanges=this.retrieveSuffixRanges()},a.fn.justifiedGallery=function(c){return this.each(function(d,e){var f=a(e);f.addClass("justified-gallery");var g=f.data("jg.controller");if("undefined"==typeof g){if("undefined"!=typeof c&&null!==c&&"object"!==a.type(c)){if("destroy"===c)return;throw"The argument must be an object"}g=new b(f,a.extend({},a.fn.justifiedGallery.defaults,c)),f.data("jg.controller",g)}else if("norewind"===c);else{if("destroy"===c)return void g.destroy();g.updateSettings(c),g.rewind()}g.updateEntries("norewind"===c)&&g.init()})},a.fn.justifiedGallery.defaults={sizeRangeSuffixes:{},thumbnailPath:void 0,rowHeight:120,maxRowHeight:-1,margins:1,border:-1,lastRow:"nojustify",justifyThreshold:.75,fixedHeight:!1,waitThumbnailsLoad:!0,captions:!0,cssAnimation:!1,imagesAnimationDuration:500,captionSettings:{animationDuration:500,visibleOpacity:.7,nonVisibleOpacity:0},rel:null,target:null,extension:/\.[^.\\/]+$/,refreshTime:200,refreshSensitivity:0,randomize:!1,sort:!1,filter:!1,selector:"> a, > div:not(.spinner)"}}(jQuery);
\ No newline at end of file diff --git a/library/justifiedGallery/justifiedGallery.css b/library/justifiedGallery/justifiedGallery.css index 0d45475ce..99be92ff2 100644 --- a/library/justifiedGallery/justifiedGallery.css +++ b/library/justifiedGallery/justifiedGallery.css @@ -1,5 +1,5 @@ /*! - * Justified Gallery - v3.6.0 + * Justified Gallery - v3.6.1 * http://miromannino.github.io/Justified-Gallery/ * Copyright (c) 2015 Miro Mannino * Licensed under the MIT license. diff --git a/library/justifiedGallery/justifiedGallery.min.css b/library/justifiedGallery/justifiedGallery.min.css index d7b1c6726..09ae4e1f0 100644 --- a/library/justifiedGallery/justifiedGallery.min.css +++ b/library/justifiedGallery/justifiedGallery.min.css @@ -1,5 +1,5 @@ /*! - * Justified Gallery - v3.6.0 + * Justified Gallery - v3.6.1 * http://miromannino.github.io/Justified-Gallery/ * Copyright (c) 2015 Miro Mannino * Licensed under the MIT license. diff --git a/mod/blocks.php b/mod/blocks.php index 96005e9a3..3c9274991 100644 --- a/mod/blocks.php +++ b/mod/blocks.php @@ -95,7 +95,9 @@ function blocks_content(&$a) { 'ptlabel' => t('Block Name'), 'profile_uid' => intval($owner), 'expanded' => true, - 'novoting' => true + 'novoting' => true, + 'bbco_autocomplete' => 'bbcode', + 'bbcode' => true ); if($_REQUEST['title']) diff --git a/mod/bookmarks.php b/mod/bookmarks.php index 02fe2f2e1..5c48ce5a9 100644 --- a/mod/bookmarks.php +++ b/mod/bookmarks.php @@ -4,6 +4,8 @@ function bookmarks_init(&$a) { if(! local_channel()) return; $item_id = intval($_REQUEST['item']); + $burl = trim($_REQUEST['burl']); + if(! $item_id) return; @@ -36,7 +38,14 @@ function bookmarks_init(&$a) { killme(); } foreach($terms as $t) { - bookmark_add($u,$s[0],$t,$item['item_private']); + if($burl) { + if($burl == $t['url']) { + bookmark_add($u,$s[0],$t,$item['item_private']); + } + } + else + bookmark_add($u,$s[0],$t,$item['item_private']); + info( t('Bookmark added') . EOL); } } diff --git a/mod/cal.php b/mod/cal.php index b58f3a1f1..56d65d3f2 100755 --- a/mod/cal.php +++ b/mod/cal.php @@ -45,6 +45,11 @@ function cal_init(&$a) { function cal_content(&$a) { + if((get_config('system','block_public')) && (! local_channel()) && (! remote_channel())) { + return; + } + + $channel = null; if(argc() > 1) { diff --git a/mod/channel.php b/mod/channel.php index 88c420366..2ef911bbb 100644 --- a/mod/channel.php +++ b/mod/channel.php @@ -124,15 +124,18 @@ function channel_content(&$a, $update = 0, $load = false) { $x = array( 'is_owner' => $is_owner, - 'allow_location' => ((($is_owner || $observer) && (intval(get_pconfig(App::$profile['profile_uid'],'system','use_browser_location')))) ? true : false), - 'default_location' => (($is_owner) ? App::$profile['channel_location'] : ''), - 'nickname' => App::$profile['channel_address'], - 'lockstate' => (((strlen(App::$profile['channel_allow_cid'])) || (strlen(App::$profile['channel_allow_gid'])) || (strlen(App::$profile['channel_deny_cid'])) || (strlen(App::$profile['channel_deny_gid']))) ? 'lock' : 'unlock'), - 'acl' => (($is_owner) ? populate_acl($channel_acl,true,((App::$profile['channel_r_stream'] & PERMS_PUBLIC) ? t('Public') : '')) : ''), + 'allow_location' => ((($is_owner || $observer) && (intval(get_pconfig(App::$profile['profile_uid'],'system','use_browser_location')))) ? true : false), + 'default_location' => (($is_owner) ? App::$profile['channel_location'] : ''), + 'nickname' => App::$profile['channel_address'], + 'lockstate' => (((strlen(App::$profile['channel_allow_cid'])) || (strlen(App::$profile['channel_allow_gid'])) || (strlen(App::$profile['channel_deny_cid'])) || (strlen(App::$profile['channel_deny_gid']))) ? 'lock' : 'unlock'), + 'acl' => (($is_owner) ? populate_acl($channel_acl,true,((App::$profile['channel_r_stream'] & PERMS_PUBLIC) ? t('Public') : '')) : ''), 'showacl' => (($is_owner) ? 'yes' : ''), - 'bang' => '', + 'bang' => '', 'visitor' => (($is_owner || $observer) ? true : false), - 'profile_uid' => App::$profile['profile_uid'] + 'profile_uid' => App::$profile['profile_uid'], + 'editor_autocomplete' => true, + 'bbco_autocomplete' => 'bbcode', + 'bbcode' => true ); $o .= status_editor($a,$x); diff --git a/mod/chat.php b/mod/chat.php index 75c364008..375d069be 100644 --- a/mod/chat.php +++ b/mod/chat.php @@ -208,6 +208,12 @@ function chat_content(&$a) { $o = profile_tabs($a,((local_channel() && local_channel() == App::$profile['profile_uid']) ? true : false),App::$profile['channel_address']); + if(! feature_enabled(App::$profile['profile_uid'],'ajaxchat')) { + notice( t('Feature disabled.') . EOL); + return $o; + } + + $acl = new Zotlabs\Access\AccessList($channel); $channel_acl = $acl->get(); diff --git a/mod/display.php b/mod/display.php index e4a6a0e66..ef140d454 100644 --- a/mod/display.php +++ b/mod/display.php @@ -65,7 +65,10 @@ function display_content(&$a, $update = 0, $load = false) { 'visitor' => true, 'profile_uid' => local_channel(), 'return_path' => 'channel/' . $channel['channel_address'], - 'expanded' => true + 'expanded' => true, + 'editor_autocomplete' => true, + 'bbco_autocomplete' => 'bbcode', + 'bbcode' => true ); $o = '<div id="jot-popup">'; diff --git a/mod/editblock.php b/mod/editblock.php index 35922e483..214c495dd 100644 --- a/mod/editblock.php +++ b/mod/editblock.php @@ -115,7 +115,8 @@ function editblock_content(&$a) { '$ispublic' => ' ', // t('Visible to <strong>everybody</strong>'), '$geotag' => '', '$nickname' => $channel['channel_address'], - '$confirmdelete' => t('Delete block?') + '$confirmdelete' => t('Delete block?'), + '$bbco_autocomplete'=> (($mimetype == 'text/bbcode') ? 'bbcode' : 'comanche-block') )); $tpl = get_markup_template("jot.tpl"); @@ -174,6 +175,7 @@ function editblock_content(&$a) { '$defexpire' => '', '$feature_expire' => false, '$expires' => t('Set expiration date'), + '$bbcode' => (($mimetype == 'text/bbcode') ? true : false) )); $o .= replace_macros(get_markup_template('edpost_head.tpl'), array( diff --git a/mod/editlayout.php b/mod/editlayout.php index 9c27afa30..0b58fe5fe 100644 --- a/mod/editlayout.php +++ b/mod/editlayout.php @@ -109,7 +109,8 @@ function editlayout_content(&$a) { '$ispublic' => ' ', // t('Visible to <strong>everybody</strong>'), '$geotag' => $geotag, '$nickname' => $channel['channel_address'], - '$confirmdelete' => t('Delete layout?') + '$confirmdelete' => t('Delete layout?'), + '$bbco_autocomplete'=> 'comanche' )); diff --git a/mod/editpost.php b/mod/editpost.php index a433d91f4..397254a48 100644 --- a/mod/editpost.php +++ b/mod/editpost.php @@ -54,7 +54,9 @@ function editpost_content(&$a) { '$geotag' => $geotag, '$nickname' => $channel['channel_address'], '$expireswhen' => t('Expires YYYY-MM-DD HH:MM'), - '$confirmdelete' => t('Delete item?'), + '$confirmdelete' => t('Delete item?'), + '$editor_autocomplete'=> true, + '$bbco_autocomplete'=> 'bbcode' )); if(intval($itm[0]['item_obscured'])) { diff --git a/mod/editwebpage.php b/mod/editwebpage.php index 445c31ad4..9f6df9536 100644 --- a/mod/editwebpage.php +++ b/mod/editwebpage.php @@ -150,7 +150,8 @@ function editwebpage_content(&$a) { '$ispublic' => ' ', // t('Visible to <strong>everybody</strong>'), '$geotag' => $geotag, '$nickname' => $channel['channel_address'], - '$confirmdelete' => t('Delete webpage?') + '$confirmdelete' => t('Delete webpage?'), + '$bbco_autocomplete'=> 'bbcode', )); $tpl = get_markup_template("jot.tpl"); @@ -215,7 +216,7 @@ function editwebpage_content(&$a) { '$defexpire' => '', '$feature_expire' => false, '$expires' => t('Set expiration date'), - + '$bbco_autocomplete'=> 'bbcode' )); $o .= replace_macros(get_markup_template('edpost_head.tpl'), array( diff --git a/mod/getfile.php b/mod/getfile.php new file mode 100644 index 000000000..c0916de79 --- /dev/null +++ b/mod/getfile.php @@ -0,0 +1,97 @@ +<?php + +/** + * module: getfile + * + * used for synchronising files and photos across clones + * + * The site initiating the file operation will send a sync packet to known clones. + * They will respond by building the DB structures they require, then will provide a + * post request to this site to grab the file data. This is sent as a stream direct to + * disk at the other end, avoiding memory issues. + * + * Since magic-auth cannot easily be used by the CURL process at the other end, + * we will require a signed request which includes a timestamp. This should not be + * used without SSL and is potentially vulnerable to replay if an attacker decrypts + * the SSL traffic fast enough. The amount of time slop is configurable but defaults + * to 3 minutes. + * + */ + + + +require_once('include/Contact.php'); +require_once('include/attach.php'); + +function getfile_post(&$a) { + + $hash = $_POST['hash']; + $time = $_POST['time']; + $sig = $_POST['signature']; + $resource = $_POST['resource']; + $revision = intval($_POST['revision']); + + if(! $hash) + killme(); + + $channel = channelx_by_hash($hash); + + if((! $channel) || (! $time) || (! $sig)) + killme(); + + $slop = intval(get_pconfig($channel['channel_id'],'system','getfile_time_slop')); + if($slop < 1) + $slop = 3; + + $d1 = datetime_convert('UTC','UTC',"now + $slop minutes"); + $d2 = datetime_convert('UTC','UTC',"now - $slop minutes"); + + if(($time > $d1) || ($time < $d2)) { + logger('time outside allowable range'); + killme(); + } + + if(! rsa_verify($hash . '.' . $time,base64url_decode($sig),$channel['channel_pubkey'])) { + logger('verify failed.'); + killme(); + } + + + $r = attach_by_hash($resource,$revision); + + if(! $r['success']) { + notice( $r['message'] . EOL); + return; + } + + + $unsafe_types = array('text/html','text/css','application/javascript'); + + if(in_array($r['data']['filetype'],$unsafe_types)) { + header('Content-type: text/plain'); + } + else { + header('Content-type: ' . $r['data']['filetype']); + } + + header('Content-disposition: attachment; filename="' . $r['data']['filename'] . '"'); + if(intval($r['data']['os_storage'])) { + $fname = dbunescbin($r['data']['data']); + if(strpos($fname,'store') !== false) + $istream = fopen($fname,'rb'); + else + $istream = fopen('store/' . $channel['channel_address'] . '/' . $fname,'rb'); + $ostream = fopen('php://output','wb'); + if($istream && $ostream) { + pipe_streams($istream,$ostream); + fclose($istream); + fclose($ostream); + } + } + else + echo dbunescbin($r['data']['data']); + killme(); + + + +}
\ No newline at end of file diff --git a/mod/help.php b/mod/help.php index a266dbf7f..fb0339cd9 100644 --- a/mod/help.php +++ b/mod/help.php @@ -84,7 +84,21 @@ function doc_rank_sort($s1,$s2) { } +function load_context_help() { + + $path = App::$cmd; + $args = App::$argv; + + while($path) { + $context_help = load_doc_file('doc/context/' . $path . '/help.html'); + if($context_help) + break; + array_pop($args); + $path = implode($args,'/'); + } + return $context_help; +} function store_doc_file($s) { diff --git a/mod/import.php b/mod/import.php index e0c2ffa82..b14b97777 100644 --- a/mod/import.php +++ b/mod/import.php @@ -165,7 +165,6 @@ function import_account(&$a, $account_id) { logger('import step 2'); $_SESSION['import_step'] = 2; - ref_session_write(session_id(), serialize($_SESSION)); } @@ -181,7 +180,6 @@ function import_account(&$a, $account_id) { logger('import step 3'); $_SESSION['import_step'] = 3; - ref_session_write(session_id(), serialize($_SESSION)); } @@ -193,7 +191,6 @@ function import_account(&$a, $account_id) { } logger('import step 4'); $_SESSION['import_step'] = 4; - ref_session_write(session_id(), serialize($_SESSION)); } if($completed < 5) { @@ -225,7 +222,6 @@ function import_account(&$a, $account_id) { } logger('import step 5'); $_SESSION['import_step'] = 5; - ref_session_write(session_id(), serialize($_SESSION)); } @@ -262,7 +258,6 @@ function import_account(&$a, $account_id) { } logger('import step 6'); $_SESSION['import_step'] = 6; - ref_session_write(session_id(), serialize($_SESSION)); } if($completed < 7) { @@ -323,7 +318,7 @@ function import_account(&$a, $account_id) { } logger('import step 7'); $_SESSION['import_step'] = 7; - ref_session_write(session_id(), serialize($_SESSION)); + } @@ -399,7 +394,6 @@ function import_account(&$a, $account_id) { } logger('import step 8'); $_SESSION['import_step'] = 8; - ref_session_write(session_id(), serialize($_SESSION)); } @@ -449,7 +443,6 @@ function import_account(&$a, $account_id) { } logger('import step 9'); $_SESSION['import_step'] = 9; - ref_session_write(session_id(), serialize($_SESSION)); } if(is_array($data['obj'])) diff --git a/mod/layouts.php b/mod/layouts.php index dbb005e08..e28c9a066 100644 --- a/mod/layouts.php +++ b/mod/layouts.php @@ -122,7 +122,8 @@ function layouts_content(&$a) { 'profile_uid' => intval($owner), 'expanded' => true, 'placeholdertitle' => t('Layout Description (Optional)'), - 'novoting' => true + 'novoting' => true, + 'bbco_autocomplete' => 'comanche' ); if($_REQUEST['title']) diff --git a/mod/network.php b/mod/network.php index e195ac496..5465cd064 100644 --- a/mod/network.php +++ b/mod/network.php @@ -169,7 +169,10 @@ function network_content(&$a, $update = 0, $load = false) { 'acl' => populate_acl((($private_editing) ? $def_acl : $channel_acl), true, (($channel['channel_r_stream'] & PERMS_PUBLIC) ? t('Public') : '')), 'bang' => (($private_editing) ? '!' : ''), 'visitor' => true, - 'profile_uid' => local_channel() + 'profile_uid' => local_channel(), + 'editor_autocomplete' => true, + 'bbco_autocomplete' => 'bbcode', + 'bbcode' => true ); if($deftag) $x['pretext'] = $deftag; diff --git a/mod/notes.php b/mod/notes.php index 4bb97fc9e..9bf37d0f9 100644 --- a/mod/notes.php +++ b/mod/notes.php @@ -6,8 +6,18 @@ function notes_init(&$a) { return; $ret = array('success' => true); - if($_REQUEST['note_text'] || $_REQUEST['note_text'] == '') { + if(array_key_exists('note_text',$_REQUEST)) { $body = escape_tags($_REQUEST['note_text']); + + // I've had my notes vanish into thin air twice in four years. + // Provide a backup copy if there were contents previously + // and there are none being saved now. + + if(! $body) { + $old_text = get_pconfig(local_channel(),'notes','text'); + if($old_text) + set_pconfig(local_channel(),'notes','text.bak',$old_text); + } set_pconfig(local_channel(),'notes','text',$body); } diff --git a/mod/photos.php b/mod/photos.php index 0adbf752a..bf904db22 100644 --- a/mod/photos.php +++ b/mod/photos.php @@ -126,6 +126,34 @@ function photos_post(&$a) { if($_REQUEST['dropalbum'] == t('Delete Album')) { + + // This is dangerous because we combined file storage and photos into one interface + // This function will remove all photos from any directory with the same name since + // we have not passed the path value. + + // The correct solution would be to use a full pathname from your storage root for 'album' + // We also need to prevent/block removing the storage root folder. + + $folder_hash = ''; + + $r = q("select * from attach where is_dir = 1 and uid = %d and filename = '%s'", + intval($page_owner_uid), + dbesc($album) + ); + if(! $r) { + notice( t('Album not found.') . EOL); + return; + } + if(count($r) > 1) { + notice( t('Multiple storage folders exist with this album name, but within different directories. Please remove the desired folder or folders using the Files manager') . EOL); + return; + } + else { + $folder_hash = $r[0]['hash']; + } + + + $res = array(); // get the list of photos we are about to delete @@ -149,9 +177,6 @@ function photos_post(&$a) { if($r) { foreach($r as $i) { attach_delete($page_owner_uid, $i['resource_id'], 1 ); - // This is now being done in attach_delete() - // drop_item($i['id'],false,DROPITEM_PHASE1,true /* force removal of linked items */); - // proc_run('php','include/notifier.php','drop',$i['id']); } } @@ -163,6 +188,15 @@ function photos_post(&$a) { // @FIXME do the same for the linked attach + if($folder_hash) { + attach_delete($page_owner_uid,$folder_hash, 1); + + $sync = attach_export_data(App::$data['channel'],$folder_hash, true); + + if($sync) + build_sync_packet($page_owner_uid,array('file' => array($sync))); + } + } goaway(z_root() . '/photos/' . App::$data['channel']['channel_address']); @@ -183,23 +217,11 @@ function photos_post(&$a) { ); if($r) { -/* - q("DELETE FROM `photo` WHERE `uid` = %d AND `resource_id` = '%s'", - intval($page_owner_uid), - dbesc($r[0]['resource_id']) - ); -*/ attach_delete($page_owner_uid, $r[0]['resource_id'], 1 ); -/* - $i = q("SELECT * FROM `item` WHERE `resource_id` = '%s' AND resource_type = 'photo' and `uid` = %d LIMIT 1", - dbesc($r[0]['resource_id']), - intval($page_owner_uid) - ); - if(count($i)) { - drop_item($i[0]['id'],true,DROPITEM_PHASE1); - $url = z_root(); - } -*/ + $sync = attach_export_data(App::$data['channel'],$r[0]['resource_id'], true); + + if($sync) + build_sync_packet($page_owner_uid,array('file' => array($sync))); } goaway(z_root() . '/photos/' . App::$data['channel']['channel_address'] . '/album/' . $_SESSION['album_return']); @@ -218,7 +240,7 @@ function photos_post(&$a) { $acl->set_from_array($_POST); $perm = $acl->get(); - $resource_id = App::$argv[2]; + $resource_id = argv(2); if(! strlen($albname)) $albname = datetime_convert('UTC',date_default_timezone_get(),'now', 'Y'); @@ -443,6 +465,11 @@ function photos_post(&$a) { goaway(z_root() . '/' . $_SESSION['photo_return']); return; // NOTREACHED + $sync = attach_export_data(App::$data['channel'],$resource_id); + + if($sync) + build_sync_packet($page_owner_uid,array('file' => array($sync))); + } @@ -555,8 +582,8 @@ function photos_content(&$a) { $o = ""; - $o .= "<script> var profile_uid = " . App::$profile['profile_uid'] - . "; var netargs = '?f='; var profile_page = " . App::$pager['page'] . "; </script>\r\n"; + $o .= "<script> var profile_uid = " . App::$profile['profile_uid'] + . "; var netargs = '?f='; var profile_page = " . App::$pager['page'] . "; </script>\r\n"; // tabs diff --git a/mod/wall_attach.php b/mod/wall_attach.php index 75786b479..7f054705f 100644 --- a/mod/wall_attach.php +++ b/mod/wall_attach.php @@ -23,23 +23,6 @@ function wall_attach_post(&$a) { $observer = App::get_observer(); -// if($_FILES['userfile']['tmp_name']) { -// $x = @getimagesize($_FILES['userfile']['tmp_name']); -// logger('getimagesize: ' . print_r($x,true), LOGGER_DATA); -// if(($x) && ($x[2] === IMAGETYPE_GIF || $x[2] === IMAGETYPE_JPEG || $x[2] === IMAGETYPE_PNG)) { -// $args = array( 'source' => 'editor', 'visible' => 0, 'contact_allow' => array($channel['channel_hash'])); -// $ret = photo_upload($channel,$observer,$args); -// if($ret['success']) { -// echo "\n\n" . $ret['body'] . "\n\n"; -// killme(); -// } -// if($using_api) -// return; -// notice($ret['message']); -// killme(); -// } -// } - $def_album = get_pconfig($channel['channel_id'],'system','photo_path'); $def_attach = get_pconfig($channel['channel_id'],'system','attach_path'); diff --git a/mod/webpages.php b/mod/webpages.php index 2196eb9fc..c20a147da 100644 --- a/mod/webpages.php +++ b/mod/webpages.php @@ -114,7 +114,9 @@ function webpages_content(&$a) { 'mimetype' => $mimetype, 'layout' => $layout, 'expanded' => true, - 'novoting' => true + 'novoting'=> true, + 'bbco_autocomplete' => 'bbcode', + 'bbcode' => true ); if($_REQUEST['title']) diff --git a/util/add_addon_repo b/util/add_addon_repo index decd9e091..a8dd9f49a 100755 --- a/util/add_addon_repo +++ b/util/add_addon_repo @@ -1,10 +1,21 @@ #!/bin/bash -f -if [ $# -ne 2 ]; then +if [ $# -lt 2 ]; then echo usage: $0 repo_url nickname exit 1 fi +if [[ $1 != *"//github.com/redmatrix"* && $3 != 'insecure' ]]; then + echo ""; + echo "This is NOT an official project repository."; + echo "In order to protect you from unverified and"; + echo "possibly malicious content, this repository"; + echo "will not be linked to your site unless you"; + echo "append the word 'insecure' to the command."; + echo ""; + exit 1 +fi + mkdir -p extend/addon/$2 mkdir addon > /dev/null 2>&1 git clone $1 extend/addon/$2 @@ -14,7 +25,6 @@ fi filelist=(`ls extend/addon/$2`) - cd addon for a in "${filelist[@]}" ; do base=`basename $a` diff --git a/util/add_theme_repo b/util/add_theme_repo index d41eba6d9..8280c447b 100755 --- a/util/add_theme_repo +++ b/util/add_theme_repo @@ -1,11 +1,21 @@ #!/bin/bash -f - -if [ $# -ne 2 ]; then +if [ $# -lt 2 ]; then echo usage: $0 repo_url nickname exit 1 fi +if [[ $1 != *"//github.com/redmatrix"* && $3 != 'insecure' ]]; then + echo ""; + echo "This is NOT an official project repository."; + echo "In order to protect you from unverified and"; + echo "possibly malicious content, this repository"; + echo "will not be linked to your site unless you"; + echo "append the word 'insecure' to the command."; + echo ""; + exit 1 +fi + mkdir -p extend/theme/$2 git clone $1 extend/theme/$2 if [ $? -ne 0 ]; then diff --git a/util/add_widget_repo b/util/add_widget_repo index 347e8e4e1..e7e316ba4 100755 --- a/util/add_widget_repo +++ b/util/add_widget_repo @@ -1,10 +1,21 @@ #!/bin/bash -f -if [ $# -ne 2 ]; then +if [ $# -lt 2 ]; then echo usage: $0 repo_url nickname exit 1 fi +if [[ $1 != *"//github.com/redmatrix"* && $3 != 'insecure' ]]; then + echo ""; + echo "This is NOT an official project repository."; + echo "In order to protect you from unverified and"; + echo "possibly malicious content, this repository"; + echo "will not be linked to your site unless you"; + echo "append the word 'insecure' to the command."; + echo ""; + exit 1 +fi + mkdir -p extend/widget/$2 mkdir widget > /dev/null 2>&1 git clone $1 extend/widget/$2 diff --git a/version.inc b/version.inc index d4e2b9f6e..4c06763d5 100644 --- a/version.inc +++ b/version.inc @@ -1,2 +1,3 @@ 2016-04-12.1364H + diff --git a/view/css/conversation.css b/view/css/conversation.css index 39c973c14..304e0f196 100644 --- a/view/css/conversation.css +++ b/view/css/conversation.css @@ -296,3 +296,9 @@ a.wall-item-name-link { .event-label { font-weight: bold; } + +/* bb-code */ + +.overline { + text-decoration: overline; +} diff --git a/view/en/htconfig.tpl b/view/en/htconfig.tpl index 13c5aa942..4aa6132a6 100644 --- a/view/en/htconfig.tpl +++ b/view/en/htconfig.tpl @@ -43,6 +43,7 @@ App::$config['system']['location_hash'] = '{{$site_id}}'; App::$config['system']['transport_security_header'] = 1; App::$config['system']['content_security_policy'] = 1; +App::$config['system']['ssl_cookie_protection'] = 1; // Your choices are REGISTER_OPEN, REGISTER_APPROVE, or REGISTER_CLOSED. // Be certain to create your own personal account before setting diff --git a/view/js/autocomplete.js b/view/js/autocomplete.js index 437425a0e..a4a1fdf51 100644 --- a/view/js/autocomplete.js +++ b/view/js/autocomplete.js @@ -100,6 +100,66 @@ function submit_form(e) { $(e).parents('form').submit(); } +function getWord(text, caretPos) { + var index = text.indexOf(caretPos); + var postText = text.substring(caretPos, caretPos+8); + if ((postText.indexOf("[/list]") > 0) || postText.indexOf("[/ul]") > 0 || postText.indexOf("[/ol]") > 0) { + return postText; + } +} + +function getCaretPosition(ctrl) { + var CaretPos = 0; // IE Support + if (document.selection) { + ctrl.focus(); + var Sel = document.selection.createRange(); + Sel.moveStart('character', -ctrl.value.length); + CaretPos = Sel.text.length; + } + // Firefox support + else if (ctrl.selectionStart || ctrl.selectionStart == '0') + CaretPos = ctrl.selectionStart; + return (CaretPos); +} + +function setCaretPosition(ctrl, pos){ + if(ctrl.setSelectionRange) { + ctrl.focus(); + ctrl.setSelectionRange(pos,pos); + } + else if (ctrl.createTextRange) { + var range = ctrl.createTextRange(); + range.collapse(true); + range.moveEnd('character', pos); + range.moveStart('character', pos); + range.select(); + } +} + +function listNewLineAutocomplete(id) { + var text = document.getElementById(id); + var caretPos = getCaretPosition(text) + var word = getWord(text.value, caretPos); + if (word != null) { + var textBefore = text.value.substring(0, caretPos); + var textAfter = text.value.substring(caretPos, text.length); + $('#' + id).val(textBefore + '\r\n[*] ' + textAfter); + setCaretPosition(text, caretPos + 5); + return true; + } +} + +function string2bb(element) { + if(element == 'bold') return 'b'; + else if(element == 'italic') return 'i'; + else if(element == 'underline') return 'u'; + else if(element == 'overline') return 'o'; + else if(element == 'strike') return 's'; + else if(element == 'superscript') return 'sup'; + else if(element == 'subscript') return 'sub'; + else return element; +} + /** * jQuery plugin 'editor_autocomplete' */ @@ -197,3 +257,70 @@ function submit_form(e) { a.on('textComplete:select', function(e, value, strategy) { onselect(value); }); }; })( jQuery ); + +(function( $ ) { + $.fn.bbco_autocomplete = function(type) { + + if(type=='bbcode') { + var open_close_elements = ['bold', 'italic', 'underline', 'overline', 'strike', 'superscript', 'subscript', 'quote', 'code', 'spoiler', 'map', 'nobb', 'list', 'ul', 'ol', 'li', 'table', 'tr', 'th', 'td', 'center', 'color', 'font', 'size', 'zrl', 'zmg', 'rpost', 'qr', 'observer']; + var open_elements = ['observer.baseurl', 'observer.address', 'observer.photo', 'observer.name', 'observer.webname', 'observer.url', '*', 'hr', ]; + + var elements = open_close_elements.concat(open_elements); + } + + if(type=='comanche') { + var open_close_elements = ['region', 'layout', 'template', 'theme', 'widget', 'block', 'menu', 'var', 'css', 'js', 'authored', 'comment', 'webpage']; + var open_elements = []; + + var elements = open_close_elements.concat(open_elements); + } + + if(type=='comanche-block') { + var open_close_elements = ['menu', 'var']; + var open_elements = []; + + var elements = open_close_elements.concat(open_elements); + } + + bbco = { + match: /\[(\w*\**)$/, + search: function (term, callback) { + callback($.map(elements, function (element) { + return element.indexOf(term) === 0 ? element : null; + })); + }, + index: 1, + replace: function (element) { + element = string2bb(element); + if(open_elements.indexOf(element) < 0) { + if(element === 'list' || element === 'ol' || element === 'ul') { + return ['\[' + element + '\]' + '\n\[*\] ', '\n\[/' + element + '\]']; + } + else if(element === 'table') { + return ['\[' + element + '\]' + '\n\[tr\]', '\[/tr\]\n\[/' + element + '\]']; + } + else { + return ['\[' + element + '\]', '\[/' + element + '\]']; + } + } + else { + return '\[' + element + '\] '; + } + } + }; + + this.attr('autocomplete','off'); + var a = this.textcomplete([bbco], {className:'acpopup', zIndex:1020}); + + a.on('textComplete:select', function(e, value, strategy) { value; }); + + $(this).keypress(function(e){ + if (e.keyCode == 13) { + x = listNewLineAutocomplete(this.id); + if(x) + e.preventDefault(); + } + }); + }; +})( jQuery ); + diff --git a/view/js/main.js b/view/js/main.js index 04b317914..799ae82bc 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -491,6 +491,7 @@ function updateConvItems(mode,data) { if(isVisible) showHideComments(itmId); $("> .wall-item-outside-wrapper .autotime, > .thread-wrapper .autotime",this).timeago(); + $("> .shared_header .autotime",this).timeago(); } else { $('img',this).each(function() { @@ -502,6 +503,7 @@ function updateConvItems(mode,data) { if(isVisible) showHideComments(itmId); $("> .wall-item-outside-wrapper .autotime, > .thread-wrapper .autotime",this).timeago(); + $("> .shared_header .autotime",this).timeago(); } prev = ident; }); @@ -529,6 +531,7 @@ function updateConvItems(mode,data) { if(isVisible) showHideComments(itmId); $("> .wall-item-outside-wrapper .autotime, > .thread-wrapper .autotime",this).timeago(); + $("> .shared_header .autotime",this).timeago(); } else { $('img',this).each(function() { @@ -540,6 +543,7 @@ function updateConvItems(mode,data) { if(isVisible) showHideComments(itmId); $("> .wall-item-outside-wrapper .autotime, > .thread-wrapper .autotime",this).timeago(); + $("> .shared_header .autotime",this).timeago(); } }); @@ -573,6 +577,7 @@ function updateConvItems(mode,data) { if(isVisible) showHideComments(itmId); $("> .wall-item-outside-wrapper .autotime, > .thread-wrapper .autotime",this).timeago(); + $("> .shared_header .autotime",this).timeago(); } prev = ident; }); @@ -617,6 +622,8 @@ function updateConvItems(mode,data) { /* autocomplete @nicknames */ $(".comment-edit-form textarea").editor_autocomplete(baseurl+"/acl?f=&n=1"); + /* autocomplete bbcode */ + $(".comment-edit-form textarea").bbco_autocomplete('bbcode'); var bimgs = ((preloadImages) ? false : $(".wall-item-body img").not(function() { return this.complete; })); var bimgcount = bimgs.length; @@ -1046,6 +1053,7 @@ function preview_comment(id) { function(data) { if(data.preview) { $("#comment-edit-preview-" + id).html(data.preview); + $("#comment-edit-preview-" + id + " .autotime").timeago(); $("#comment-edit-preview-" + id + " a").click(function() { return false; }); } }, @@ -1076,6 +1084,7 @@ function preview_post() { function(data) { if(data.preview) { $("#jot-preview-content").html(data.preview); + $("#jot-preview-content .autotime").timeago(); $("#jot-preview-content" + " a").click(function() { return false; }); } }, diff --git a/view/js/mod_chat.js b/view/js/mod_chat.js index 0d47e3e77..f9d2a599c 100644 --- a/view/js/mod_chat.js +++ b/view/js/mod_chat.js @@ -15,4 +15,7 @@ $(document).ready(function() { $('#jot-public').show(); } }).trigger('change'); + + $('#chatText').bbco_autocomplete('bbcode'); + }); diff --git a/view/js/mod_events.js b/view/js/mod_events.js index 0b7b3d24c..74b811dd6 100644 --- a/view/js/mod_events.js +++ b/view/js/mod_events.js @@ -3,9 +3,8 @@ */ $(document).ready( function() { - enableDisableFinishDate(); - + $('#comment-edit-text-desc, #comment-edit-text-loc').bbco_autocomplete('bbcode'); }); function enableDisableFinishDate() { diff --git a/view/js/mod_mail.js b/view/js/mod_mail.js index 561df7229..3e55c8aeb 100644 --- a/view/js/mod_mail.js +++ b/view/js/mod_mail.js @@ -3,4 +3,5 @@ $(document).ready(function() { $("#recip-complete").val(data.xid); }); $(".autotime").timeago() + $('#prvmail-text').bbco_autocomplete('bbcode'); }); diff --git a/view/js/mod_photos.js b/view/js/mod_photos.js index d371c3f2f..34e2e3f25 100644 --- a/view/js/mod_photos.js +++ b/view/js/mod_photos.js @@ -11,6 +11,8 @@ $(document).ready(function() { $("#photo-edit-newtag").val('@' + data.name); }); + $('#id_body').bbco_autocomplete('bbcode'); + $('#contact_allow, #contact_deny, #group_allow, #group_deny').change(function() { var selstr; $('#contact_allow option:selected, #contact_deny option:selected, #group_allow option:selected, #group_deny option:selected').each( function() { diff --git a/view/js/mod_profiles.js b/view/js/mod_profiles.js index aad2ca902..a7754e0c5 100644 --- a/view/js/mod_profiles.js +++ b/view/js/mod_profiles.js @@ -1,3 +1,4 @@ $(document).ready(function() { $('form').areYouSure(); // Warn user about unsaved settings + $('textarea').bbco_autocomplete('bbcode'); }); diff --git a/view/tpl/head.tpl b/view/tpl/head.tpl index ba883c1f0..ab229eaf7 100755 --- a/view/tpl/head.tpl +++ b/view/tpl/head.tpl @@ -16,7 +16,7 @@ <link rel="search" href="{{$baseurl}}/opensearch" type="application/opensearchdescription+xml" - title="Search in the Hubzilla" /> + title="{{$osearch}}" /> <script> diff --git a/view/tpl/jot-header.tpl b/view/tpl/jot-header.tpl index 84fccc105..b8618ab69 100755 --- a/view/tpl/jot-header.tpl +++ b/view/tpl/jot-header.tpl @@ -11,10 +11,15 @@ function initEditor(cb){ if(plaintext == 'none') { $("#profile-jot-text-loading").spin(false).hide(); $("#profile-jot-text").css({ 'height': 200, 'color': '#000' }); + {{if $bbco_autocomplete}} + $("#profile-jot-text").bbco_autocomplete('{{$bbco_autocomplete}}'); // autocomplete bbcode + {{/if}} + {{if $editor_autocomplete}} if(typeof channelId === 'undefined') $("#profile-jot-text").editor_autocomplete(baseurl+"/acl"); else $("#profile-jot-text").editor_autocomplete(baseurl+"/acl",[channelId]); // Also gives suggestions from current channel's connections + {{/if}} editor = true; $("a#jot-perms-icon").colorbox({ 'inline' : true, @@ -213,20 +218,6 @@ function enableOnUser(){ } } - function jotVideoURL() { - reply = prompt("{{$vidurl}}"); - if(reply && reply.length) { - addeditortext('[video]' + reply + '[/video]'); - } - } - - function jotAudioURL() { - reply = prompt("{{$audurl}}"); - if(reply && reply.length) { - addeditortext('[audio]' + reply + '[/audio]'); - } - } - function jotGetLocation() { reply = prompt("{{$whereareu}}", $('#jot-location').val()); if(reply && reply.length) { diff --git a/view/tpl/jot.tpl b/view/tpl/jot.tpl index dea75efa9..026c586a0 100755 --- a/view/tpl/jot.tpl +++ b/view/tpl/jot.tpl @@ -48,6 +48,7 @@ {{/if}} <div id="profile-jot-submit-wrapper" class="jothidden"> <div id="profile-jot-submit-left" class="btn-toolbar pull-left"> + {{if $bbcode}} <div class="btn-group"> <button id="main-editor-bold" class="btn btn-default btn-sm" title="{{$bold}}" onclick="inserteditortag('b', 'profile-jot-text'); return false;"> <i class="icon-bold jot-icons"></i> @@ -65,6 +66,7 @@ <i class="icon-terminal jot-icons"></i> </button> </div> + {{/if}} {{if $visitor}} <div class="btn-group hidden-xs"> {{if $writefiles}} diff --git a/view/tpl/login.tpl b/view/tpl/login.tpl index da38f3571..d56c8f272 100755 --- a/view/tpl/login.tpl +++ b/view/tpl/login.tpl @@ -5,7 +5,7 @@ <div id="login-input" class="form-group"> {{include file="field_input.tpl" field=$lname}} {{include file="field_password.tpl" field=$lpassword}} - {{include file="field_checkbox.tpl" field=$remember}} + {{include file="field_checkbox.tpl" field=$remember_me}} <button type="submit" name="submit" class="btn btn-block btn-primary">{{$login}}</button> </div> <div id="login-extra-links"> diff --git a/view/tpl/nav.tpl b/view/tpl/nav.tpl index 3d6809c22..886f73947 100755 --- a/view/tpl/nav.tpl +++ b/view/tpl/nav.tpl @@ -1,4 +1,45 @@ - <div class="container-fluid"> +<script> + $(document).mouseup(function (e) + { + var container = $("#help-content"); + + if (!container.is(e.target) // if the target of the click isn't the container... + && container.has(e.target).length === 0 // ... nor a descendant of the container + && container.hasClass('help-content-open')) + { + container.removeClass('help-content-open'); + } + }); +</script> +<style> +.help-content { + background: rgba(255, 255, 255, 0.9); + color: #333333; + position: fixed; + top: 50px; + left: -80%; + width: 80%; + height: 60%; + padding: 20px; + transition: left 300ms cubic-bezier(0.17, 0.04, 0.03, 0.94); + box-sizing: border-box; + border: #CCC thin solid; + overflow: auto; +} + +.help-content-open { + left: 0px; + -moz-box-shadow: 3px 3px 3px #ccc; + -webkit-box-shadow: 3px 3px 3px #ccc; + box-shadow: 3px 3px 3px #ccc; +} + +.help-content dd { + margin-bottom: 1em; +} +</style> + +<div class="container-fluid"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-1"> <span class="icon-bar"></span> @@ -189,8 +230,13 @@ {{if $nav.help}} <li class="{{$sel.help}}"> - <a class="{{$nav.help.2}}" target="hubzilla-help" href="{{$nav.help.0}}" title="{{$nav.help.3}}" id="{{$nav.help.4}}"><i class="icon-question"></i></a> + <a class="{{$nav.help.2}}" target="hubzilla-help" href="{{$nav.help.0}}" title="{{$nav.help.3}}" id="{{$nav.help.4}}" onclick="$('#help-content').toggleClass('help-content-open'); return false;"><i class="icon-question"></i></a> </li> + + <div id="help-content" class="help-content"> + {{$nav.help.5}} + <p class="pull-right"><a href="{{$nav.help.0}}">Click here for more documentation...</a></p> + </div> {{/if}} </ul> </div> diff --git a/view/tpl/pdledit.tpl b/view/tpl/pdledit.tpl index af8e37602..3e1f5a3fc 100644 --- a/view/tpl/pdledit.tpl +++ b/view/tpl/pdledit.tpl @@ -18,4 +18,7 @@ <input type="submit" name="submit" value="{{$submit}}" /> </form> +<script> + $('textarea').bbco_autocomplete('comanche'); +</script> </div> |