diff options
author | Mario <mario@mariovavti.com> | 2020-12-14 11:02:20 +0000 |
---|---|---|
committer | Mario <mario@mariovavti.com> | 2020-12-14 11:02:20 +0000 |
commit | 2a154f8c9a772d61e7dabb5e3fd110ba00cc6007 (patch) | |
tree | f57522c894e96f5a386f2fea9c379702b4c2781e | |
parent | 634ace552d40f9f287a6419dd6fe5b19d3f390ca (diff) | |
download | volse-hubzilla-2a154f8c9a772d61e7dabb5e3fd110ba00cc6007.tar.gz volse-hubzilla-2a154f8c9a772d61e7dabb5e3fd110ba00cc6007.tar.bz2 volse-hubzilla-2a154f8c9a772d61e7dabb5e3fd110ba00cc6007.zip |
merge branch files_ng into dev
30 files changed, 1684 insertions, 587 deletions
@@ -1,3 +1,8 @@ +Hubzilla 5.0.5 (2020-12-12) + - Fix hubloc issue in mod getfile + - Remove duplicate SQL query + + Hubzilla 5.0.4 (2020-12-01) - Fix regression updating the primary - Dismiss title in response activities @@ -34,7 +39,7 @@ Hubzilla 5.0.2 (2020-11-16) Hubzilla 5.0.1 (2020-11-12) - - Fix share title size + - Fix share title size - Fix issue where hublocs could get mixed up between different protocols Addons @@ -350,7 +355,7 @@ Hubzilla 4.2 (2019-06-04) Hubzilla 4.0.3 (2019-04-26) - Add attachments to zot6 event objects - - Add zot6 to federated transports + - Add zot6 to federated transports - Update import/export to handle zot6 hublocs and xchans - Update fix_system_urls() to handle zot6 hublocs - Fix infinite loop using postgres as backend @@ -403,7 +408,7 @@ Hubzilla 4.0.1 (2019-03-21) - Perform zot6 discovery in import_author_xchan - Fix authenticated fetches - Port zot_record_preferred() from zap - + Addons: - Pubcrawl: deliver comments to abook contacts and thread participants - Pubcrawl: fix can_comment_on_post() @@ -617,7 +622,7 @@ Hubzilla 3.8.3 (2018-11-05) - Fix forum notifications count not correct - Fix gallery addon which broke mod apps in some situations - Fix wiki_list widget not working on every page respectively level - + Hubzilla 3.8.2 (2018-10-29) - Merge unmerged changes from dev into master @@ -633,7 +638,7 @@ Hubzilla 3.8.2 (2018-10-29) - Look for for matches in the entire string when suggesting emojis - Add [summary] bbcode to autocomplete list - Update blueimp_upload to version 9.23 - - Update spanish strings + - Update spanish strings Addons - Cart: don't allow items to be added unless user is logged into the Grid. @@ -691,7 +696,7 @@ Hubzilla 3.8 (2018-10-19) - Sanitise vcard fields - Don't sync system apps - + Bugfixes - Fix issue with timeago plurals - Fix issue with HTTP signatures @@ -750,7 +755,7 @@ Hubzilla 3.8 (2018-10-19) Hubzilla 3.6 (2018-07-25) - Update jquery.timeago library - Implement Hookable CSP - - ActivityStreams: accept header changes to support plume + - ActivityStreams: accept header changes to support plume - Streamline inconsistencies in addon naming - SECURITY: hash the session_id in logs - Update justified gallery library @@ -763,9 +768,9 @@ Hubzilla 3.6 (2018-07-25) - Make droping posts of removed connections more memory efficient - Refactor getOutainfo() for DAV storage - Optionally report total available space when uploading - - SECURITY: provide option to disable the cloud 'root' directory and make the cloud module require a target channel nickname + - SECURITY: provide option to disable the cloud 'root' directory and make the cloud module require a target channel nickname - Add plink and llink to viewsource - - Add new 'filter by name' feature + - Add new 'filter by name' feature - Remove network tabs - New activity filter widget - New activity order widget @@ -807,8 +812,8 @@ Hubzilla 3.6 (2018-07-25) - Fix sys channels visible in dirsearch - Fix remote_self not working correctly - Fix photos not syncing properly if destination is a postgres site - - Fix wrong hubloc_url for activitypub hublocs - - Fix z_check_dns() for BSD + - Fix wrong hubloc_url for activitypub hublocs + - Fix z_check_dns() for BSD - Fix not null violation in oauth1 - Fix DB issues with oauth2 on postgresql - Fix 'anybody authenticated' not correctly handled in can_comment_on_post() @@ -1370,10 +1375,10 @@ Hubzilla 2.6.2 (2017-08-31) Hubzilla 2.6.1 (2017-08-18) - Fix a regression with dav clients - Raise install requirements - + Plugins/Addon - Diaspora: fix PHP warning - - GNU-Social: fix PHP warning + - GNU-Social: fix PHP warning Hubzilla 2.6 (2017-08-16) @@ -1381,18 +1386,18 @@ Hubzilla 2.6 (2017-08-16) - Consolidate disable_discover_tab config - Fix some bbcode to markdown conversion issues - Improved finding of recursive attachment permissions - - Smaller line-height for notification badges + - Smaller line-height for notification badges - Bluegrid schema removed - will be added again if someone is willing to maintain it - Improved file_activity() - DB - add index for item.obj_type - Add options flag to bb_to_markdown() so we can distinguish between diaspora use and other use and therefore filter and adjust content selectively - - Close the apps-menu if the notifications-menu is open and vice versa + - Close the apps-menu if the notifications-menu is open and vice versa - Remove redundant call to jquery ready function in photo albums view - Remove borders from navbar toggler in mobile view - Improve the formatting of shares when converting from bbcode to markdown - Suppress fopen errors from dav - Make local channel (not our own) nav menus appear similar to what we are used from remote channels - - Indicate the selected channel in the dropdown menu if the feature is enabled + - Indicate the selected channel in the dropdown menu if the feature is enabled - Provide a mechanism to mark apps active in the app tray - Allow wildcard tag and category searches - Improved installer @@ -1404,7 +1409,7 @@ Hubzilla 2.6 (2017-08-16) - Update htmlpurifier to version 4.9.3 - Update sabre/http to version 4.2.3 - Add optimize-autoloader to composer config - - Missing abook_{my,their}_perms in pg schema and missing keys in mysql schema + - Missing abook_{my,their}_perms in pg schema and missing keys in mysql schema - Provide a gender icon on the profile sidebar within reason - Provide more comprehensible information on the admin summary page - Upgrade blueimp from 9.8 to 9.18 @@ -1470,7 +1475,7 @@ Hubzilla 2.6 (2017-08-16) Cdav addon moved to core head_add_css() needs a preceding '/' to find files in the addons dir New addon code syntax highlighting (moved from core to addon) - Pubsubhubbub: specify a minimum number of records - otherwise it defaults to zero + Pubsubhubbub: specify a minimum number of records - otherwise it defaults to zero Hubzilla 2.4 (2017-05-31) @@ -1824,10 +1829,10 @@ Hubzilla 1.14 (2016-10-13) - Start grouping addons by server_role Hubzilla 1.12 - - extensible permissions so you can create a new permission rule such as "can write to my wiki" or "can see me naked". - - guest access tokens can do anything you let them, including create posts and administer your channel + - extensible permissions so you can create a new permission rule such as "can write to my wiki" or "can see me naked". + - guest access tokens can do anything you let them, including create posts and administer your channel - ACLs can be set on files and directories prior to creation. - - ACL tool can now be used in multiple forms within a page + - ACL tool can now be used in multiple forms within a page - a myriad of new drag/drop features (drop files or photos into /cloud or a post, or drop link into a post or comment, etc.) - multiple file uploads - improvements to website import @@ -1852,7 +1857,7 @@ Hubzilla 1.10 Wiki: Lots of enhanced functionality, usability improvements, and bugfixes from v1.8 Turned into an optional feature (default on) but disabled in UNO - Sync: + Sync: Items are now relocated (links patched) when syncing to clones Access Tokens: New feature - allows members to create access controlled guest logins and create/share 'dropbox' style links to protected resources. @@ -1860,7 +1865,7 @@ Hubzilla 1.10 Use icons instead of iconic text constructs Only request geolocation permission when creating a post, not on page load provide 'redeliver' option on Delivery Report page for when things really stuff up - CalDAV/CardDAV management pages with heaps of functionality + CalDAV/CardDAV management pages with heaps of functionality Lib: z_fetch_url() updated to accept different request methods and request bodies item_store(), item_store_update() now return the stored items @@ -1884,7 +1889,7 @@ Hubzilla 1.10 issues with 'use existing photo' for profile photo layout editor "list all layouts" returned empty oembed - better detect video file URLs so they aren't loaded into memory. - handcrafted bbcode tables could end up with way too much whitespace due to CRLF translation + handcrafted bbcode tables could end up with way too much whitespace due to CRLF translation refresh permissions whitescreen in 1.8 force immediate profile photo update on local site regression: 'save bookmarks' post action missing @@ -1909,7 +1914,7 @@ Hubzilla 1.8 Documentation: Clarify privacy rights of commenters w/r/t conversation owners, as this policy is network dependent. Wiki (Git backed): - Brand new feature. We'll call it experimental until it has undergone a bit more testing. + Brand new feature. We'll call it experimental until it has undergone a bit more testing. Account Cloning: Regression on clone channel creation created a new channel name each time. New issue (fixed) with directory creation on cloned file content @@ -1932,7 +1937,7 @@ Hubzilla 1.8 Experimental PDO database driver Creation of Daemon Master class and port all daemon (background task) interfaces to use it Create separate class for each of 'Cron', 'Cron daily', and 'Cron weekly'. - Always run a Cron maintenance task if not run in the last four hours + Always run a Cron maintenance task if not run in the last four hours Refactor the template classes Refactor the ConversationItem mess into ThreadItem and ThreadStream Refactor Apps, Enotify, and Chat library code @@ -1940,7 +1945,7 @@ Hubzilla 1.8 Created WebServer class for top level Remove mcrypt dependencies (deprecated in PHP 7.1) Remove all reserved (including merely 'not recommended') words as DB table column names - Provide mutex lock on DB logging to prevent recursion under rare failure modes. + Provide mutex lock on DB logging to prevent recursion under rare failure modes. Bugfixes: Remove db_close function on page end - not needed and will not work with persistent DB connections. Undefined ref_session_write @@ -1960,7 +1965,7 @@ Hubzilla 1.8 CalDAV/CardDAV plugin provided Issue sending Diaspora 'like' activities from sources that did not propagate the DCV Allow 'superblock' to work across API calls from third party clients - statistics.json: use 'zot' as protocol + statistics.json: use 'zot' as protocol Issues fixed during testing of ability to follow Diaspora tags Parse issue with Diaspora reshare content Chess: moved to main repo, ported to 1.8 @@ -1972,7 +1977,7 @@ Hubzilla 1.6 Plugin hook interface adapted to call static class methods Context help improved dramatically with content for the most accessed pages. Reverted a compatibility change to support GNU-social events. We copied their feed format and their feed format is wrong (XML namespace collisions). - Provide a querystring attribute to CSS/JS resources to avoid caching issues when our code changes (which is often). + Provide a querystring attribute to CSS/JS resources to avoid caching issues when our code changes (which is often). Fix javascript detection and allow either positive or negative detection. Refactor the plugin hook registration procedure, provide 'unregister all' ability. Fix RSD (Real Simple Discovery) which has been broken for some time. @@ -1981,7 +1986,7 @@ Hubzilla 1.6 Update font-awesome to 4.6.1 Update SabreDAV to 3.0 (PHP version requirements prevent us from pushing it further at this time) Help text added to cmdline utilities config and pconfig - Reworking of the database logging facility to avoid the rare but troublesome recursion when the log facility needed to query the DB internally to obtain config parameters. + Reworking of the database logging facility to avoid the rare but troublesome recursion when the log facility needed to query the DB internally to obtain config parameters. Implement singleton delivery (emulate nomadic identity to singleton networks and services) Fix empty album name in photo activities when photo is stored in top level folder. Allow engineering units to be used in service class data size restrictions (400M, 1G, etc.) @@ -1989,7 +1994,7 @@ Hubzilla 1.6 Admin interface provided to manage external resource repositories Oembed security reworked. Now all sources are filtered by default unless blocked. Remove the date-string version and use only STD_VERSION - Add categories and categorisation filtering and the ability to edit all apps (including system apps) for a given channel + Add categories and categorisation filtering and the ability to edit all apps (including system apps) for a given channel Ensure the ability to translate names of all system apps (except those provided in addons) Provide ability to add categories to content from channel sources Lots of work on the presentation of the ACL widget to enhance usability and intuitiveness @@ -2003,24 +2008,24 @@ Hubzilla 1.6 Provide some extra security checks to import data and files to prevent mischief Block CalDAV/CardDAV namespace reserved words from being used as a channel nickname/redress since Sabre is somewhat inflexible in this regard Plugins: - Diaspora - markdown translator work needed to eradicate the Diaspora Comment Virus. + Diaspora + markdown translator work needed to eradicate the Diaspora Comment Virus. upgrade all inbound paths with the most recent protocol changes (several of these) convert 'diaspora_meta' (Diaspora Comment Virus) to iconfig and eradicate from sites with Diaspora disabled implement social relay and allow following tags upgrade statistics.json to NodeInfo. Currently hubzilla sites are tagged as 'redmatrix' because the NodeInfo schema lacks extensibility and project names are used to designate protocol compatibility rather than protocol names. Std-embeds - New addon to allow a handful of corporate providers to run unfiltered embed code (youtube, vimeo, soundcloud) + New addon to allow a handful of corporate providers to run unfiltered embed code (youtube, vimeo, soundcloud) Various: upgrade font-awesome icons and adapt a few addons to Objects and the new hook interface and new controller interface - + Hubzilla 1.4 [This list may appear brief, but encompasses a huge amount of re-writing and re-factoring of the internal code structure to gain long-term performance and stability and provide a standard interface to alternate protocol federation plugins which were made possible by the UNO configuration. - UNO is a configuration of hubzilla introduced in 1.3 with reduced complexity and which provides - improved protocol federation potential to other networks by virtue of removing nomadic identity - (which is not possible to model or work around using other network protocols).] + UNO is a configuration of hubzilla introduced in 1.3 with reduced complexity and which provides + improved protocol federation potential to other networks by virtue of removing nomadic identity + (which is not possible to model or work around using other network protocols).] Implement channel move operation for UNO configuration Remove bookmark references in UNO (which has no bookmarks by default) @@ -2043,10 +2048,10 @@ Hubzilla 1.4 Rework detection of JavaScript to avoid reload penalty under normal operation Changed primary directory server to a hubzilla server Plugins: - Diaspora - switch to alternate XML parser to avoid storing compound objects + Diaspora - switch to alternate XML parser to avoid storing compound objects GNU-Social - Huge amounts of work, federation somewhat working now, several issues remain Friendica - Initial federation work (not yet published) - + Hubzilla 1.3 Admin Security configuration page created which consolidates several previously hidden settings: Communication white/black lists @@ -2062,26 +2067,26 @@ Hubzilla 1.3 "pubsites" module UI reworked item-meta ("iconfig") created which implements arbitrary storage for item metadata for plugins abook-meta ("abconfig") created which implements arbitrary storage for connection metadata for plugins - "Strict transport security header" made optional as it conflicts with some existing Apache/nginx configurations - "Hubzilla UNO" (Hubzilla with radically simplified and locked site settings) implemented as an install configuration. + "Strict transport security header" made optional as it conflicts with some existing Apache/nginx configurations + "Hubzilla UNO" (Hubzilla with radically simplified and locked site settings) implemented as an install configuration. .well-known directory conflict worked out to support LetsEncrypt cert ownership checks without disrupting webfinger and other internal uses of .well-known Lots of work on 'zcards' which are self-contained HTML representations of a channel including cover photos, profile photos, and some text information Long standing bug uncovered which failed to properly restrict the lower time limit for public feed requests A number of fixes to "readmore" to fix page jumping Bugfix: persons other than the channel owner who have permission to upload photos to a channel could not do so if the js_upload plugin/addon was enabled Siteinfo incorrectly identifying secondary directory servers - Allow admin to set and lock features when UNO is configured + Allow admin to set and lock features when UNO is configured Atom feeds: alter how events are formatted to be compatible with GNU-social Allow guest/visitor access to view personal calendar Moved several more classes to "composer format" and provided an autoloader. Bugfix: require existing password to change password - Bugfix: allow relative_date() to be translated to Polish which has more than two plural forms. + Bugfix: allow relative_date() to be translated to Polish which has more than two plural forms. Plugin API: add "requires" keyword to module header to indicate dependent addons ActivityStreams improvements and cleanup: photo and file activities UI cleanup for editing profile when multiple profiles enabled Removed the "markdown" feature as there are numerous issues and no maintainer. Provide "footer" bbcode to ease theming of post footer content - Bugfix: install issues caused by composer code refactor and typo in postgres load file + Bugfix: install issues caused by composer code refactor and typo in postgres load file Plugins: keepout - "block public on steroids" pubsubhubbub - provides PuSH support to Atom feeds, required for GNU-social federation @@ -2089,7 +2094,7 @@ Hubzilla 1.3 Diaspora protocol - some work to ease migration to the new signing format Diaspost - disabled; numerous issues and no maintainer smileybutton - theme work and fixed compatibility with other jot-tools plugins - + Hubzilla 1.2 Provide extra HTTP security headers (several of them). @@ -2099,7 +2104,7 @@ Hubzilla 1.2 Add locked features to siteinfo report to aid remote debugging Provide version compatibility checking to plugins (minversion, maxversion, and minphpversion) Account config storage - Provide optional integrated registration and channel create form + Provide optional integrated registration and channel create form cli utility for managing addons issue with sharing photo "items" cover photo manager: upload, crop, and store @@ -2121,7 +2126,7 @@ Hubzilla 1.2 proc_run modified to use exec() instead of proc_open() - causing issues on some PHP installations remote delegation failure under a specific set of circumstances which we were finally able to duplicate Delegation section of Channel Manager was missing names and contained useless notification icons. - Change "expire" channel setting to show system limit if there is one. + Change "expire" channel setting to show system limit if there is one. Regression: provide a one-click ignore of pending connection Config to control directory keyword generation on client and server. "Collections" renamed to "Privacy Groups", documentation improved @@ -2174,11 +2179,11 @@ Hubzilla 1.1 Addons/Plugins: Pageheader addon ported from Friendica Hubwall (allow admin to send email to all accounts on this hub) created - GNU-social - queueing added - Diaspora - fixes for various failures to update profile photos, updates to queue API + GNU-social - queueing added + Diaspora - fixes for various failures to update profile photos, updates to queue API Cross Domain Authenticated Chess (Andrew Manning's repository) - - And... the normal "lots of bugs fixed, translations updated, and documentation improved" - - + And... the normal "lots of bugs fixed, translations updated, and documentation improved" + + + diff --git a/Zotlabs/Lib/Libsync.php b/Zotlabs/Lib/Libsync.php index 72a9afc48..7b968532a 100644 --- a/Zotlabs/Lib/Libsync.php +++ b/Zotlabs/Lib/Libsync.php @@ -713,7 +713,7 @@ class Libsync { if($arr['locations']) { if($absolute) - self::check_location_move($sender['hash'],$arr['locations']); + Libzot::check_location_move($sender['hash'],$arr['locations']); $xisting = q("select * from hubloc where hubloc_hash = '%s'", dbesc($sender['hash']) diff --git a/Zotlabs/Module/Attach_edit.php b/Zotlabs/Module/Attach_edit.php new file mode 100644 index 000000000..0a41dbb22 --- /dev/null +++ b/Zotlabs/Module/Attach_edit.php @@ -0,0 +1,212 @@ +<?php +namespace Zotlabs\Module; +/** + * @file Zotlabs/Module/Attach_edit.php + * + */ + +use App; +use Zotlabs\Web\Controller; +use Zotlabs\Lib\Libsync; +use Zotlabs\Access\AccessList; + +class Attach_edit extends Controller { + + function post() { + + if (!local_channel() && !remote_channel()) { + return; + } + + $attach_ids = ((x($_POST, 'attach_ids')) ? $_POST['attach_ids'] : []); + $attach_id = ((x($_POST, 'attach_id')) ? intval($_POST['attach_id']) : 0); + $channel_id = ((x($_POST, 'channel_id')) ? intval($_POST['channel_id']) : 0); + $dnd = ((x($_POST, 'dnd')) ? intval($_POST['dnd']) : 0); + $permissions = ((x($_POST, 'permissions')) ? intval($_POST['permissions']) : 0); + $return_path = ((x($_POST, 'return_path')) ? notags($_POST['return_path']) : 'cloud'); + $delete = ((x($_POST, 'delete')) ? intval($_POST['delete']) : 0); + $newfolder = ((x($_POST, 'newfolder_' . $attach_id)) ? notags($_POST['newfolder_' . $attach_id]) : ''); + if(! $newfolder) + $newfolder = ((x($_POST, 'newfolder')) ? notags($_POST['newfolder']) : ''); + $newfilename = ((x($_POST, 'newfilename_' . $attach_id)) ? notags($_POST['newfilename_' . $attach_id]) : ''); + $recurse = ((x($_POST, 'recurse_' . $attach_id)) ? intval($_POST['recurse_' . $attach_id]) : 0); + if(! $recurse) + $recurse = ((x($_POST, 'recurse')) ? intval($_POST['recurse']) : 0); + $notify = ((x($_POST, 'notify_edit_' . $attach_id)) ? intval($_POST['notify_edit_' . $attach_id]) : 0); + $copy = ((x($_POST, 'copy_' . $attach_id)) ? intval($_POST['copy_' . $attach_id]) : 0); + if(! $copy) + $copy = ((x($_POST, 'copy')) ? intval($_POST['copy']) : 0); + + $categories = ((x($_POST, 'categories_' . $attach_id)) ? notags($_POST['categories_' . $attach_id]) : ''); + if(! $categories) + $categories = ((x($_POST, 'categories')) ? notags($_POST['categories']) : ''); + + if($attach_id) + $attach_ids[] = $attach_id; + + $single = ((count($attach_ids) === 1) ? true : false); + + $channel = channelx_by_n($channel_id); + + if (! $channel) { + notice(t('Channel not found.') . EOL); + return; + } + + $nick = $channel['channel_address']; + $observer = App::get_observer(); + $observer_hash = (($observer) ? $observer['xchan_hash'] : ''); + $is_owner = ((local_channel() == $channel_id) ? true : false); + + $ids_str = implode(',', $attach_ids); + + $r = q("SELECT id, uid, hash, creator, folder, filename, is_photo, is_dir FROM attach WHERE id IN ( %s ) AND uid = %d", + dbesc($ids_str), + intval($channel_id) + ); + + if (! $r) { + notice(t('File not found.') . EOL); + return; + } + + foreach ($r as $rr) { + $actions_done = ''; + $attach_id = $rr['id']; + $resource = $rr['hash']; + $creator = $rr['creator']; + $folder = $rr['folder']; + $filename = $rr['filename']; + $is_photo = intval($rr['is_photo']); + $is_dir = intval($rr['is_dir']); + $admin_delete = false; + + $is_creator = (($creator == $observer_hash) ? true : false); + $move = ((! $copy && ($folder !== $newfolder || (($single) ? $filename !== $newfilename : false))) ? true : false); + + $perms = get_all_perms($channel_id, $observer_hash); + + if (! ($perms['view_storage'] || is_site_admin())) { + notice( t('Permission denied.') . EOL); + continue; + } + + if (! $perms['write_storage']) { + if (is_site_admin()) { + $admin_delete = true; + } + else { + notice( t('Permission denied.') . EOL); + continue; + } + } + + if (!$is_owner && !$admin_delete) { + if(! $is_creator) { + notice( t('Permission denied.') . EOL); + continue; + } + } + + if ($delete) { + attach_delete($channel_id, $resource, $is_photo); + + q("DELETE FROM term WHERE uid = %d AND oid = %d AND otype = %d", + intval($channel_id), + intval($attach_id), + intval(TERM_OBJ_FILE) + ); + + $actions_done .= 'delete,'; + + } + + if ($copy) { + if($is_dir && $resource == $newfolder) { + notice( t('Can not copy folder into itself.') . EOL); + continue; + } + $x = attach_copy($channel_id, $resource, $newfolder, (($single) ? $newfilename : '')); + if ($x['success']) + $resource = $x['resource_id']; + + $actions_done .= 'copy,'; + + } + + if ($move) { + if($is_dir && $resource == $newfolder) { + notice( sprintf(t('Can not move folder "%s" into itself.'), $filename) . EOL); + continue; + } + $x = attach_move($channel_id, $resource, $newfolder, (($single) ? $newfilename : '')); + + $actions_done .= 'move,'; + + } + + if(! $delete && ! $dnd) { + if ($single || (! $single && $categories)) { + q("DELETE FROM term WHERE uid = %d AND oid = %d AND otype = %d", + intval($channel_id), + intval($attach_id), + intval(TERM_OBJ_FILE) + ); + $cat = explode(',', $categories); + if ($cat) { + foreach($cat as $term) { + $term = trim(escape_tags($term)); + if ($term) { + $term_link = z_root() . '/cloud/' . $nick . '/?cat=' . $term; + store_item_tag($channel_id, $attach_id, TERM_OBJ_FILE, TERM_CATEGORY, $term, $term_link); + } + } + } + $actions_done .= 'cat_add,'; + + } + else { + q("DELETE FROM term WHERE uid = %d AND oid = %d AND otype = %d", + intval($channel_id), + intval($attach_id), + intval(TERM_OBJ_FILE) + ); + $actions_done .= 'cat_remove,'; + } + + if ($is_owner && ($single || (! $single && $permissions))) { + $acl = new AccessList($channel); + $acl->set_from_array($_REQUEST); + $x = $acl->get(); + + attach_change_permissions($channel_id, $resource, $x['allow_cid'], $x['allow_gid'], $x['deny_cid'], $x['deny_gid'], $recurse, true); + $actions_done .= 'permissions,'; + + if ($notify) { + attach_store_item($channel, $observer, $resource); + $actions_done .= 'notify,'; + } + } + } + + if (! $admin_delete && $actions_done) { + $sync = attach_export_data($channel, $resource, false); + + if ($sync) { + Libsync::build_sync_packet($channel_id, ['file' => [$sync]]); + } + } + + logger('attach_edit: ' . $actions_done); + + } + + if($dnd || $delete) { + json_return_and_die([ 'success' => true ]); + } + + goaway($return_path); + + } + +} diff --git a/Zotlabs/Module/Cloud.php b/Zotlabs/Module/Cloud.php index f595e0fac..39ae0f92f 100644 --- a/Zotlabs/Module/Cloud.php +++ b/Zotlabs/Module/Cloud.php @@ -8,7 +8,11 @@ namespace Zotlabs\Module; */ use Sabre\DAV as SDAV; -use \Zotlabs\Storage; +use \Zotlabs\Web\Controller; +use \Zotlabs\Storage\BasicAuth; +use \Zotlabs\Storage\Directory; +use \Zotlabs\Storage\Browser; + // composer autoloader for SabreDAV require_once('vendor/autoload.php'); @@ -20,7 +24,7 @@ require_once('include/attach.php'); * @brief Cloud Module. * */ -class Cloud extends \Zotlabs\Web\Controller { +class Cloud extends Controller { /** * @brief Fires up the SabreDAV server. @@ -42,7 +46,7 @@ class Cloud extends \Zotlabs\Web\Controller { - $auth = new \Zotlabs\Storage\BasicAuth(); + $auth = new BasicAuth(); $ob_hash = get_observer_hash(); @@ -72,7 +76,7 @@ class Cloud extends \Zotlabs\Web\Controller { if($x !== \App::$query_string) goaway(z_root() . '/' . $x); - $rootDirectory = new \Zotlabs\Storage\Directory('/', $auth); + $rootDirectory = new Directory('/', [], $auth); // A SabreDAV server-object $server = new SDAV\Server($rootDirectory); @@ -85,7 +89,7 @@ class Cloud extends \Zotlabs\Web\Controller { $is_readable = false; // provide a directory view for the cloud in Hubzilla - $browser = new \Zotlabs\Storage\Browser($auth); + $browser = new Browser($auth); $auth->setBrowserPlugin($browser); $server->addPlugin($browser); @@ -105,13 +109,13 @@ class Cloud extends \Zotlabs\Web\Controller { if($browser->build_page) construct_page(); - + killme(); } function DAVException($err) { - + if($err instanceof \Sabre\DAV\Exception\NotFound) { notice( t('Not found') . EOL); } @@ -126,7 +130,7 @@ class Cloud extends \Zotlabs\Web\Controller { } construct_page(); - + killme(); } diff --git a/Zotlabs/Module/Dav.php b/Zotlabs/Module/Dav.php index 11950dda0..949b89950 100644 --- a/Zotlabs/Module/Dav.php +++ b/Zotlabs/Module/Dav.php @@ -100,7 +100,7 @@ class Dav extends \Zotlabs\Web\Controller { $auth->setRealm(ucfirst(\Zotlabs\Lib\System::get_platform_name()) . ' ' . 'WebDAV'); - $rootDirectory = new \Zotlabs\Storage\Directory('/', $auth); + $rootDirectory = new \Zotlabs\Storage\Directory('/', [], $auth); // A SabreDAV server-object $server = new SDAV\Server($rootDirectory); diff --git a/Zotlabs/Module/File_upload.php b/Zotlabs/Module/File_upload.php index 1735e9487..6794dceee 100644 --- a/Zotlabs/Module/File_upload.php +++ b/Zotlabs/Module/File_upload.php @@ -11,17 +11,16 @@ require_once('include/photos.php'); class File_upload extends \Zotlabs\Web\Controller { function post() { - logger('file upload: ' . print_r($_REQUEST,true)); logger('file upload: ' . print_r($_FILES,true)); - + $channel = (($_REQUEST['channick']) ? channelx_by_nick($_REQUEST['channick']) : null); - + if(! $channel) { logger('channel not found'); killme(); } - + $_REQUEST['source'] = 'file_upload'; if($channel['channel_id'] != local_channel()) { @@ -40,13 +39,11 @@ class File_upload extends \Zotlabs\Web\Controller { $r = attach_mkdir($channel, get_observer_hash(), $_REQUEST); if($r['success']) { $hash = $r['data']['hash']; - $sync = attach_export_data($channel,$hash); if($sync) { Libsync::build_sync_packet($channel['channel_id'],array('file' => array($sync))); } - goaway(z_root() . '/cloud/' . $channel['channel_address'] . '/' . $r['data']['display_path']); - + goaway(z_root() . '/' . $_REQUEST['return_url']); } } else { @@ -54,8 +51,6 @@ class File_upload extends \Zotlabs\Web\Controller { $matches = []; $partial = false; - - if(array_key_exists('HTTP_CONTENT_RANGE',$_SERVER)) { $pm = preg_match('/bytes (\d*)\-(\d*)\/(\d*)/',$_SERVER['HTTP_CONTENT_RANGE'],$matches); if($pm) { @@ -83,7 +78,7 @@ class File_upload extends \Zotlabs\Web\Controller { ]; } } - else { + else { if(! array_key_exists('userfile',$_FILES)) { $_FILES['userfile'] = [ 'name' => $_FILES['files']['name'], @@ -103,8 +98,9 @@ class File_upload extends \Zotlabs\Web\Controller { } } + goaway(z_root() . '/' . $_REQUEST['return_url']); - + } - + } diff --git a/Zotlabs/Module/Filestorage.php b/Zotlabs/Module/Filestorage.php index 0c6233493..0d132e998 100644 --- a/Zotlabs/Module/Filestorage.php +++ b/Zotlabs/Module/Filestorage.php @@ -11,6 +11,9 @@ class Filestorage extends \Zotlabs\Web\Controller { function post() { + notice( t('Deprecated!') . EOL); + return; + $channel_id = ((x($_POST, 'uid')) ? intval($_POST['uid']) : 0); if((! $channel_id) || (! local_channel()) || ($channel_id != local_channel())) { @@ -47,6 +50,9 @@ class Filestorage extends \Zotlabs\Web\Controller { function get() { + notice( t('Deprecated!') . EOL); + return; + if(argc() > 1) $which = argv(1); else { @@ -88,7 +94,7 @@ class Filestorage extends \Zotlabs\Web\Controller { } else { notice( t('Permission denied.') . EOL); - if($json_return) + if($json_return) json_return_and_die([ 'success' => false ]); return; } @@ -102,24 +108,23 @@ class Filestorage extends \Zotlabs\Web\Controller { if(! $r) { notice( t('File not found.') . EOL); - if($json_return) + if($json_return) json_return_and_die([ 'success' => false ]); goaway(z_root() . '/cloud/' . $which); } - if(local_channel() !== $owner) { + if((local_channel() !== $owner) && !$admin_delete) { if($r[0]['creator'] && $r[0]['creator'] !== $ob_hash) { notice( t('Permission denied.') . EOL); - if($json_return) + if($json_return) json_return_and_die([ 'success' => false ]); goaway(z_root() . '/cloud/' . $which); } } - $f = $r[0]; $channel = channelx_by_n($owner); @@ -138,7 +143,7 @@ class Filestorage extends \Zotlabs\Web\Controller { if($json_return) json_return_and_die([ 'success' => true ]); - goaway(dirname($url)); + //goaway(dirname($url)); } diff --git a/Zotlabs/Module/Rpost.php b/Zotlabs/Module/Rpost.php index f03dae2bf..031270845 100644 --- a/Zotlabs/Module/Rpost.php +++ b/Zotlabs/Module/Rpost.php @@ -10,7 +10,7 @@ require_once('include/zot.php'); /** * remote post - * + * * https://yoursite/rpost?f=&title=&body=&remote_return= * * This can be called via either GET or POST, use POST for long body content as suhosin often limits GET parameter length @@ -20,7 +20,7 @@ require_once('include/zot.php'); * body= Body of post * url= URL which will be parsed and the results appended to the body * source= Source application - * post_id= post_id of post to 'share' (local use only) + * post_id= post_id of post to 'share' (local use only) * remote_return= absolute URL to return after posting is finished * type= choices are 'html' or 'bbcode', default is 'bbcode' * @@ -32,16 +32,16 @@ require_once('include/zot.php'); class Rpost extends \Zotlabs\Web\Controller { function get() { - + $o = ''; - + if(! local_channel()) { if(remote_channel()) { // redirect to your own site. // We can only do this with a GET request so you'll need to keep the text short or risk getting truncated // by the wretched beast called 'suhosin'. All the browsers now allow long GET requests, but suhosin // blocks them. - + $url = get_rpost_path(\App::get_observer()); // make sure we're not looping to our own hub if(($url) && (! stristr($url, \App::get_hostname()))) { @@ -53,10 +53,10 @@ class Rpost extends \Zotlabs\Web\Controller { goaway($url); } } - + // The login procedure is going to bugger our $_REQUEST variables // so save them in the session. - + if(array_key_exists('body',$_REQUEST)) { $_SESSION['rpost'] = $_REQUEST; } @@ -64,14 +64,14 @@ class Rpost extends \Zotlabs\Web\Controller { } nav_set_selected('Post'); - + // If we have saved rpost session variables, but nothing in the current $_REQUEST, recover the saved variables - + if((! array_key_exists('body',$_REQUEST)) && (array_key_exists('rpost',$_SESSION))) { $_REQUEST = $_SESSION['rpost']; unset($_SESSION['rpost']); } - + if(array_key_exists('channel',$_REQUEST)) { $r = q("select channel_id from channel where channel_account_id = %d and channel_address = '%s' limit 1", intval(get_account_id()), @@ -82,7 +82,7 @@ class Rpost extends \Zotlabs\Web\Controller { $change = change_channel($r[0]['channel_id']); } } - + if($_REQUEST['remote_return']) { $_SESSION['remote_return'] = $_REQUEST['remote_return']; } @@ -91,21 +91,27 @@ class Rpost extends \Zotlabs\Web\Controller { goaway($_SESSION['remote_return']); goaway(z_root() . '/network'); } - + $plaintext = true; - + if(array_key_exists('type', $_REQUEST) && $_REQUEST['type'] === 'html') { require_once('include/html2bbcode.php'); - $_REQUEST['body'] = html2bbcode($_REQUEST['body']); + $_REQUEST['body'] = html2bbcode($_REQUEST['body']); } - + $channel = \App::get_channel(); - - - $acl = new \Zotlabs\Access\AccessList($channel); - - $channel_acl = $acl->get(); - + + if($_REQUEST['acl']) { + $acl = new \Zotlabs\Access\AccessList([]); + $acl->set($_REQUEST['acl']); + $channel_acl = $acl->get(); + } + else { + $acl = new \Zotlabs\Access\AccessList($channel); + $channel_acl = $acl->get(); + } + + if($_REQUEST['url']) { $x = z_fetch_url(z_root() . '/linkinfo?f=&url=' . urlencode($_REQUEST['url'])); if($x['success']) @@ -115,7 +121,7 @@ class Rpost extends \Zotlabs\Web\Controller { if($_REQUEST['post_id']) { $_REQUEST['body'] .= '[share=' . intval($_REQUEST['post_id']) . '][/share]'; } - + $x = array( 'is_owner' => true, 'allow_location' => ((intval(get_pconfig($channel['channel_id'],'system','use_browser_location'))) ? '1' : ''), @@ -137,19 +143,19 @@ class Rpost extends \Zotlabs\Web\Controller { 'bbcode' => true, 'jotnets' => true ); - + $editor = status_editor($a,$x,false,'Rpost'); - + $o .= replace_macros(get_markup_template('edpost_head.tpl'), array( '$title' => t('Edit post'), '$cancel' => '', '$editor' => $editor )); - + return $o; - + } - - - + + + } diff --git a/Zotlabs/Storage/Browser.php b/Zotlabs/Storage/Browser.php index fde66efcd..3d99bf659 100644 --- a/Zotlabs/Storage/Browser.php +++ b/Zotlabs/Storage/Browser.php @@ -3,6 +3,7 @@ namespace Zotlabs\Storage; use Sabre\DAV; +use App; /** * @brief Provides a DAV frontend for the webbrowser. @@ -76,49 +77,71 @@ class Browser extends DAV\Browser\Plugin { * @param string $path which should be displayed */ public function generateDirectoryIndex($path) { - // (owner_id = channel_id) is visitor owner of this directory? - $is_owner = ((local_channel() && $this->auth->owner_id == local_channel()) ? true : false); - - if ($this->auth->getTimezone()) - date_default_timezone_set($this->auth->getTimezone()); require_once('include/conversation.php'); require_once('include/text.php'); - if ($this->auth->owner_nick) { - $html = ''; - } - $files = $this->server->getPropertiesForPath($path, array( - '{DAV:}displayname', - '{DAV:}resourcetype', - '{DAV:}getcontenttype', - '{DAV:}getcontentlength', - '{DAV:}getlastmodified', - ), 1); + $nick = $this->auth->owner_nick; + $channel_id = $this->auth->owner_id; + // Is visitor owner of this directory? + $is_owner = ((local_channel() && $channel_id == local_channel()) ? true : false); + $cat = ((x($_REQUEST,'cat')) ? $_REQUEST['cat'] : ''); + + if ($this->auth->getTimezone()) { + date_default_timezone_set($this->auth->getTimezone()); + } + + $files = $this->server->getPropertiesForPath($path, [], 1); $parent = $this->server->tree->getNodeForPath($path); - $parentpath = array(); - // only show parent if not leaving /cloud/; TODO how to improve this? - if ($path && $path != "cloud") { - list($parentUri) = \Sabre\Uri\split($path); - $fullPath = \Sabre\HTTP\encodePath($this->server->getBaseUri() . $parentUri); + $arr = explode('/', $parent->os_path); + end($arr); + $folder_parent = ((isset($arr[1])) ? prev($arr) : ''); + + $folder_list = attach_folder_select_list($channel_id); + + $parent_path = ''; - $parentpath['icon'] = $this->enableAssets ? '<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl('icons/parent' . $this->iconExtension) . '" width="24" alt="' . t('parent') . '"></a>' : ''; - $parentpath['path'] = $fullPath; + $siteroot_disabled = get_config('system', 'cloud_disable_siteroot'); + + // Hide parent folder if in /cloud or category view + if (($channel_id && ! $cat) || ($siteroot_disabled && $path !== 'cloud')) { + list($parent_uri) = \Sabre\Uri\split($path); + $parent_path = \Sabre\HTTP\encodePath($this->server->getBaseUri() . $parent_uri); } - $f = array(); + $is_root_folder = (($path === 'cloud/' . $nick) ? true : false); + + $f = []; + foreach ($files as $file) { - $ft = array(); + + $ft = []; $type = null; - // This is the current directory, we can skip it - if (rtrim($file['href'], '/') == $path) continue; + $href = rtrim($file['href'], '/'); + + // This is the current directory - skip it + if ($href === $path) + continue; + + $node = $this->server->tree->getNodeForPath($href); + $data = $node->data; + $attach_hash = $data['hash']; + $folder_hash = $node->folder_hash; + + list(, $filename) = \Sabre\Uri\split($href); - list(, $name) = \Sabre\Uri\split($file['href']); + $name = isset($file[200]['{DAV:}displayname']) ? $file[200]['{DAV:}displayname'] : $filename; + $name = $this->escapeHTML($name); + + $size = isset($file[200]['{DAV:}getcontentlength']) ? (int)$file[200]['{DAV:}getcontentlength'] : ''; + + $lastmodified = ((isset($file[200]['{DAV:}getlastmodified'])) ? $file[200]['{DAV:}getlastmodified']->getTime()->format('Y-m-d H:i:s') : ''); if (isset($file[200]['{DAV:}resourcetype'])) { + $type = $file[200]['{DAV:}resourcetype']->getValue(); // resourcetype can have multiple values @@ -128,22 +151,22 @@ class Browser extends DAV\Browser\Plugin { // Some name mapping is preferred switch ($v) { case '{DAV:}collection' : - $type[$k] = t('Collection'); + $type[$k] = 'Collection'; break; case '{DAV:}principal' : - $type[$k] = t('Principal'); + $type[$k] = 'Principal'; break; case '{urn:ietf:params:xml:ns:carddav}addressbook' : - $type[$k] = t('Addressbook'); + $type[$k] = 'Addressbook'; break; case '{urn:ietf:params:xml:ns:caldav}calendar' : - $type[$k] = t('Calendar'); + $type[$k] = 'Calendar'; break; case '{urn:ietf:params:xml:ns:caldav}schedule-inbox' : - $type[$k] = t('Schedule Inbox'); + $type[$k] = 'Schedule Inbox'; break; case '{urn:ietf:params:xml:ns:caldav}schedule-outbox' : - $type[$k] = t('Schedule Outbox'); + $type[$k] = 'Schedule Outbox'; break; case '{http://calendarserver.org/ns/}calendar-proxy-read' : $type[$k] = 'Proxy-Read'; @@ -158,124 +181,144 @@ class Browser extends DAV\Browser\Plugin { // If no resourcetype was found, we attempt to use // the contenttype property - if (!$type && isset($file[200]['{DAV:}getcontenttype'])) { + if (! $type && isset($file[200]['{DAV:}getcontenttype'])) { $type = $file[200]['{DAV:}getcontenttype']; } - if (!$type) $type = t('Unknown'); - $size = isset($file[200]['{DAV:}getcontentlength']) ? (int)$file[200]['{DAV:}getcontentlength'] : ''; - $lastmodified = ((isset($file[200]['{DAV:}getlastmodified'])) ? $file[200]['{DAV:}getlastmodified']->getTime()->format('Y-m-d H:i:s') : ''); - - $fullPath = \Sabre\HTTP\encodePath('/' . trim($this->server->getBaseUri() . ($path ? $path . '/' : '') . $name, '/')); - - $displayName = isset($file[200]['{DAV:}displayname']) ? $file[200]['{DAV:}displayname'] : $name; - - $displayName = $this->escapeHTML($displayName); - $type = $this->escapeHTML($type); - - - $icon = ''; - - if ($this->enableAssets) { - $node = $this->server->tree->getNodeForPath(($path ? $path . '/' : '') . $name); - foreach (array_reverse($this->iconMap) as $class=>$iconName) { - if ($node instanceof $class) { - $icon = '<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl($iconName . $this->iconExtension) . '" alt="" width="24"></a>'; - break; - } - } - } - - $parentHash = ''; - $owner = $this->auth->owner_id; - $splitPath = explode('/', $fullPath); - if (count($splitPath) > 3) { - for ($i = 3; $i < count($splitPath); $i++) { - $attachName = urldecode($splitPath[$i]); - $attachHash = $this->findAttachHash($owner, $parentHash, $attachName); - $parentHash = $attachHash; - } + if (! $type) { + $type = $data['filetype']; } + $type = $this->escapeHTML($type); - // generate preview icons for tile view. + // generate preview icons for tile view. // Currently we only handle images, but this could potentially be extended with plugins - // to provide document and video thumbnails. SVG, PDF and office documents have some + // to provide document and video thumbnails. SVG, PDF and office documents have some // security concerns and should only be allowed on single-user sites with tightly controlled - // upload access. system.thumbnail_security should be set to 1 if you want to include these - // types + // upload access. system.thumbnail_security should be set to 1 if you want to include these + // types $is_creator = false; $photo_icon = ''; $preview_style = intval(get_config('system','thumbnail_security',0)); - $r = q("select content, creator from attach where hash = '%s' and uid = %d limit 1", - dbesc($attachHash), - intval($owner) - ); + $is_creator = (($data['creator'] === get_observer_hash()) ? true : false); - if($r) { - $is_creator = (($r[0]['creator'] === get_observer_hash()) ? true : false); - if(file_exists(dbunescbin($r[0]['content']) . '.thumb')) { - $photo_icon = 'data:image/jpeg;base64,' . base64_encode(file_get_contents(dbunescbin($r[0]['content']) . '.thumb')); -// logger('found thumb: ' . $photo_icon); - } - } - - if(strpos($type,'image/') === 0 && $attachHash) { - $r = q("select resource_id, imgscale from photo where resource_id = '%s' and imgscale in ( %d, %d ) order by imgscale asc limit 1", - dbesc($attachHash), + if(strpos($type,'image/') === 0 && $attach_hash) { + $p = q("select resource_id, imgscale from photo where resource_id = '%s' and imgscale in ( %d, %d ) order by imgscale asc limit 1", + dbesc($attach_hash), intval(PHOTO_RES_320), intval(PHOTO_RES_PROFILE_80) ); - if($r) { - $photo_icon = 'photo/' . $r[0]['resource_id'] . '-' . $r[0]['imgscale']; + if($p) { + $photo_icon = 'photo/' . $p[0]['resource_id'] . '-' . $p[0]['imgscale']; } if($type === 'image/svg+xml' && $preview_style > 0) { - $photo_icon = $fullPath; + $photo_icon = $href; } } - $g = [ 'resource_id' => $attachHash, 'thumbnail' => $photo_icon, 'security' => $preview_style ]; + $g = [ 'resource_id' => $attach_hash, 'thumbnail' => $photo_icon, 'security' => $preview_style ]; call_hooks('file_thumbnail', $g); $photo_icon = $g['thumbnail']; + $lockstate = (($data['allow_cid'] || $data['allow_gid'] || $data['deny_cid'] || $data['deny_gid']) ? 'lock' : 'unlock'); + $id = $data['id']; - $attachIcon = ""; // "<a href=\"attach/".$attachHash."\" title=\"".$displayName."\"><i class=\"fa fa-arrow-circle-o-down\"></i></a>"; + if($id) { + $terms = q("select * from term where oid = %d AND otype = %d", + intval($id), + intval(TERM_OBJ_FILE) + ); + + $categories = []; + $terms_str = ''; + if($terms) { + foreach($terms as $t) { + $term = htmlspecialchars($t['term'],ENT_COMPAT,'UTF-8',false) ; + if(! trim($term)) + continue; + $categories[] = array('term' => $term, 'url' => $t['url']); + if ($terms_str) + $terms_str .= ','; + $terms_str .= $term; + } + $ft['terms'] = replace_macros(get_markup_template('item_categories.tpl'),array( + '$categories' => $categories + )); + } + } // put the array for this file together - $ft['attachId'] = $this->findAttachIdByHash($attachHash); - $ft['fileStorageUrl'] = substr($fullPath, 0, strpos($fullPath, "cloud/")) . "filestorage/" . $this->auth->owner_nick; + $ft['attach_id'] = $id; $ft['icon'] = $icon; $ft['photo_icon'] = $photo_icon; - $ft['attachIcon'] = (($size) ? $attachIcon : ''); - // @todo Should this be an item value, not a global one? $ft['is_owner'] = $is_owner; $ft['is_creator'] = $is_creator; - $ft['fullPath'] = $fullPath; - $ft['displayName'] = $displayName; + $ft['rel_path'] = (($data) ? '/cloud/' . $nick .'/' . $data['display_path'] : $href); + $ft['full_path'] = z_root() . (($data) ? '/cloud/' . $nick .'/' . $data['display_path'] : $href); + $ft['name'] = $name; $ft['type'] = $type; $ft['size'] = $size; - $ft['sizeFormatted'] = userReadableSize($size); - $ft['lastmodified'] = (($lastmodified) ? datetime_convert('UTC', date_default_timezone_get(), $lastmodified) : ''); - $ft['iconFromType'] = getIconFromType($type); + $ft['collection'] = (($type === 'Collection') ? true : false); + $ft['size_formatted'] = userReadableSize($size); + $ft['last_modified'] = (($lastmodified) ? datetime_convert('UTC', date_default_timezone_get(), $lastmodified) : ''); + $ft['icon_from_type'] = getIconFromType($type); + + $ft['allow_cid'] = acl2json($data['allow_cid']); + $ft['allow_gid'] = acl2json($data['allow_gid']); + $ft['deny_cid'] = acl2json($data['deny_cid']); + $ft['deny_gid'] = acl2json($data['deny_gid']); + + $ft['raw_allow_cid'] = $data['allow_cid']; + $ft['raw_allow_gid'] = $data['allow_gid']; + $ft['raw_deny_cid'] = $data['deny_cid']; + $ft['raw_deny_gid'] = $data['deny_gid']; + + $ft['lockstate'] = $lockstate; + $ft['resource'] = $data['hash']; + $ft['folder'] = $data['folder']; + $ft['revision'] = $data['revision']; + $ft['newfilename'] = ['newfilename_' . $id, t('Change filename to'), $name]; + $ft['categories'] = ['categories_' . $id, t('Categories'), $terms_str]; + + // create a copy of the list which we can alter for the current resource + $folders = $folder_list; + if($data['is_dir']) { + // can not copy a folder into itself + unset($folders[$folder_hash]); + } + + $ft['newfolder'] = ['newfolder_' . $id, t('Select a target location'), $data['folder'], '', $folders]; + $ft['copy'] = ['copy_' . $id, t('Copy to target location'), 0, '', [t('No'), t('Yes')]]; + $ft['recurse'] = ['recurse_' . $id, t('Set permissions for all files and sub folders'), 0, '', [t('No'), t('Yes')]]; + $ft['notify'] = ['notify_edit_' . $id, t('Notify your contacts about this file'), 0, '', [t('No'), t('Yes')]]; $f[] = $ft; } - $output = ''; if ($this->enablePost) { - $this->server->emit('onHTMLActionsPanel', array($parent, &$output, $path)); + $this->server->emit('onHTMLActionsPanel', [$parent, &$output, $path]); } $deftiles = (($is_owner) ? 0 : 1); + $tiles = ((array_key_exists('cloud_tiles',$_SESSION)) ? intval($_SESSION['cloud_tiles']) : $deftiles); $_SESSION['cloud_tiles'] = $tiles; - - $html .= replace_macros(get_markup_template('cloud.tpl'), array( - '$header' => t('Files') . ": " . $this->escapeHTML($path) . "/", + + $header = (($cat) ? t('File category') . ": " . $this->escapeHTML($cat) : t('Files')); + + $channel = channelx_by_n($channel_id); + if($channel) { + $acl = new \Zotlabs\Access\AccessList($channel); + $channel_acl = $acl->get(); + $lockstate = (($acl->is_private()) ? 'lock' : 'unlock'); + } + + $html = replace_macros(get_markup_template('cloud.tpl'), array( + '$header' => $header, '$total' => t('Total'), '$actionspanel' => $output, '$shared' => t('Shared'), @@ -284,8 +327,11 @@ class Browser extends DAV\Browser\Plugin { '$is_owner' => $is_owner, '$is_admin' => is_site_admin(), '$admin_delete' => t('Admin Delete'), - '$parentpath' => $parentpath, - '$cpath' => bin2hex(\App::$query_string), + '$parentpath' => $parent_path, + '$folder_parent' => $folder_parent, + '$folder' => $parent->folder_hash, + '$is_root_folder' => $is_root_folder, + '$cpath' => bin2hex(App::$query_string), '$tiles' => intval($_SESSION['cloud_tiles']), '$entries' => $f, '$name' => t('Name'), @@ -293,17 +339,33 @@ class Browser extends DAV\Browser\Plugin { '$size' => t('Size'), '$lastmod' => t('Last Modified'), '$parent' => t('parent'), - '$edit' => t('Edit'), + '$edit' => t('Submit'), '$delete' => t('Delete'), - '$nick' => $this->auth->getCurrentUser() - )); + '$channel_id' => $channel_id, + '$cpdesc' => t('Copy/paste this code to attach file to a post'), + '$cpldesc' => t('Copy/paste this URL to link file from a web page'), + + '$categories' => ['categories', t('Categories')], + '$recurse' => ['recurse', t('Set permissions for all files and sub folders'), 0, '', [t('No'), t('Yes')]], + + '$newfolder' => ['newfolder', t('Select a target location'), $parent->folder_hash, '', $folder_list], + '$copy' => ['copy', t('Copy to target location'), 0, '', [t('No'), t('Yes')]], + '$return_path' => $path, + + '$lockstate' => $lockstate, + '$allow_cid' => acl2json($channel_acl['allow_cid']), + '$allow_gid' => acl2json($channel_acl['allow_gid']), + '$deny_cid' => acl2json($channel_acl['deny_cid']), + '$deny_gid' => acl2json($channel_acl['deny_gid']) + + )); $a = false; nav_set_selected('Files'); - \App::$page['content'] = $html; + App::$page['content'] = $html; load_pdl(); $current_theme = \Zotlabs\Render\Theme::current(); @@ -335,6 +397,7 @@ class Browser extends DAV\Browser\Plugin { // SimpleCollection, we won't need to show the panel either. if (get_class($node) === 'Sabre\\DAV\\SimpleCollection') return; + require_once('include/acl_selectors.php'); $aclselect = null; @@ -387,9 +450,38 @@ class Browser extends DAV\Browser\Plugin { $special = 'cloud/' . $this->auth->owner_nick; $count = strlen($special); + + if(strpos($path,$special) === 0) - $path = trim(substr($path,$count),'/'); + $display_path = trim(substr($path,$count),'/'); + + $breadcrumbs_html = ''; + + if($display_path && ! $_REQUEST['cat']){ + $breadcrumbs = []; + $folders = explode('/', $display_path); + $folder_hashes = explode('/', $node->os_path); + $breadcrumb_path = z_root() . '/cloud/' . $this->auth->owner_nick; + + $breadcrumbs[] = [ + 'name' => $this->auth->owner_nick, + 'hash' => '', + 'path' => $breadcrumb_path + ]; + + foreach($folders as $i => $name) { + $breadcrumb_path .= '/' . $name; + $breadcrumbs[] = [ + 'name' => $name, + 'hash' => $folder_hashes[$i], + 'path' => $breadcrumb_path + ]; + } + $breadcrumbs_html = replace_macros(get_markup_template('breadcrumb.tpl'), array( + '$breadcrumbs' => $breadcrumbs + )); + } $output .= replace_macros(get_markup_template('cloud_actionspanel.tpl'), array( '$folder_header' => t('Create new folder'), @@ -404,11 +496,11 @@ class Browser extends DAV\Browser\Plugin { '$deny_cid' => acl2json($channel_acl['deny_cid']), '$deny_gid' => acl2json($channel_acl['deny_gid']), '$lockstate' => $lockstate, - '$return_url' => \App::$cmd, - '$path' => $path, - '$folder' => find_folder_hash_by_path($this->auth->owner_id, $path), + '$return_url' => $path, + '$folder' => $node->folder_hash, '$dragdroptext' => t('Drop files here to immediately upload'), - '$notify' => ['notify', t('Show in your contacts shared folder'), 0, '', [t('No'), t('Yes')]] + '$notify' => ['notify', t('Show in your contacts shared folder'), 0, '', [t('No'), t('Yes')]], + '$breadcrumbs_html' => $breadcrumbs_html )); } @@ -453,6 +545,21 @@ class Browser extends DAV\Browser\Plugin { return $hash; } + protected function findAttachHashFlat($owner, $attachName) { + $r = q("SELECT hash FROM attach WHERE uid = %d AND filename = '%s' ORDER BY edited DESC LIMIT 1", + intval($owner), + dbesc($attachName) + ); + $hash = ''; + if ($r) { + foreach ($r as $rr) { + $hash = $rr['hash']; + } + } + + return $hash; + } + /** * @brief Returns an attachment's id for a given hash. * diff --git a/Zotlabs/Storage/Directory.php b/Zotlabs/Storage/Directory.php index 1231dfa25..f2a3a603d 100644 --- a/Zotlabs/Storage/Directory.php +++ b/Zotlabs/Storage/Directory.php @@ -25,7 +25,10 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo * @var string $red_path */ private $red_path; - private $folder_hash; + public $folder_hash; + public $data; + + /** * @brief The full path as seen in the browser. * /cloud + $red_path @@ -41,7 +44,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo * * @var string $os_path */ - private $os_path = ''; + public $os_path = ''; /** * @brief Sets up the directory node, expects a full path. @@ -49,7 +52,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo * @param string $ext_path a full path * @param BasicAuth &$auth_plugin */ - public function __construct($ext_path, &$auth_plugin) { + public function __construct($ext_path, $data, &$auth_plugin) { // $ext_path = urldecode($ext_path); logger('directory ' . $ext_path, LOGGER_DATA); $this->ext_path = $ext_path; @@ -61,6 +64,8 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo } $this->auth = $auth_plugin; $this->folder_hash = ''; + $this->data = $data; + $this->getDir(); if($this->auth->browser) { @@ -116,7 +121,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo $modulename = \App::$module; if ($this->red_path === '/' && $name === $modulename) { - return new Directory('/' . $modulename, $this->auth); + return new Directory('/' . $modulename, [], $this->auth); } $x = $this->FileData($this->ext_path . '/' . $name, $this->auth); @@ -269,8 +274,8 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo dbesc($f), dbesc(datetime_convert()), dbesc(datetime_convert()), - '', - '', + '', + '', dbesc($allow_cid), dbesc($allow_gid), dbesc($deny_cid), @@ -293,7 +298,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo else { $size = file_put_contents($f, $data); } - + // delete attach entry if file_put_contents() failed if ($size === false) { logger('file_put_contents() failed to ' . $f); @@ -374,7 +379,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo $args = array( 'resource_id' => $hash, 'album' => $album, 'os_syspath' => $f, 'os_path' => $xpath['os_path'], 'display_path' => $xpath['path'], 'filename' => $name, 'getimagesize' => $gis, 'directory' => $direct); $p = photo_upload($c[0], \App::get_observer(), $args); } - + \Zotlabs\Daemon\Master::Summon([ 'Thumbnail' , $hash ]); $sync = attach_export_data($c[0], $hash); @@ -402,13 +407,14 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo if ($r) { - // When initiated from DAV, set the 'force' flag on attach_mkdir(). This will cause the operation to report success even if the - // folder already exists. + // When initiated from DAV, set the 'force' flag on attach_mkdir(). This will cause the operation to report success even if the + // folder already exists. require_once('include/attach.php'); $result = attach_mkdir($r[0], $this->auth->observer, array('filename' => $name, 'folder' => $this->folder_hash, 'force' => true)); if($result['success']) { + $sync = attach_export_data($r[0],$result['data']['hash']); logger('createDirectory: attach_export_data returns $sync:' . print_r($sync, true), LOGGER_DEBUG); @@ -476,15 +482,16 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo public function moveInto($targetName,$sourcePath, DAV\INode $sourceNode) { - if(! $this->auth->owner_id) { - return false; - } + $channel_id = $this->auth->owner_id; + // Files have $sourceNode->data['hash'] set. For directories rely on $sourceNode->folder_hash. + $resource_id = ((isset($sourceNode->data['hash'])) ? $sourceNode->data['hash'] : $sourceNode->folder_hash); + $new_folder_hash = $this->folder_hash; - if(! ($sourceNode->data && $sourceNode->data->hash)) { + if(!$channel_id && !$resource_id) return false; - } - return attach_move($this->auth->owner_id, $sourceNode->data->hash, $this->folder_hash); + $ret = attach_move($channel_id, $resource_id, $new_folder_hash); + return $ret['success']; } @@ -515,6 +522,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo $file = trim($file, '/'); $path_arr = explode('/', $file); + if (! $path_arr) return; @@ -609,6 +617,9 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo $file = trim($file, '/'); $path_arr = explode('/', $file); + $cat = $_REQUEST['cat']; + + if (! $path_arr) return null; @@ -679,7 +690,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo $_SESSION['cloud_sort'] = 'name'; switch($_SESSION['cloud_sort']) { - case 'size': + case 'size': $suffix = ' order by is_dir desc, filesize asc '; break; // The following provides inconsistent results for directories because we re-calculate the date for directories based on the most recent change @@ -692,17 +703,34 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo break; } - $r = q("select $prefix id, uid, hash, filename, filetype, filesize, revision, folder, flags, is_dir, created, edited from attach where folder = '%s' and uid = %d $perms $suffix", - dbesc($folder), - intval($channel_id) - ); + if ($cat) { + $r = q("select $prefix attach.id, attach.uid, attach.hash, attach.filename, + attach.filetype, attach.filesize, attach.revision, attach.folder, attach.creator, + attach.flags, attach.is_dir, attach.created, attach.edited, attach.display_path, + attach.allow_cid, attach.allow_gid, attach.deny_cid, attach.deny_gid from attach + left join term on attach.id = term.oid + where term.term = '%s' and attach.uid = %d $perms $suffix", + dbesc($cat), + intval($channel_id) + ); + } + else { + $r = q("select $prefix attach.id, attach.uid, attach.hash, attach.filename, + attach.filetype, attach.filesize, attach.revision, attach.folder, attach.creator, + attach.flags, attach.is_dir, attach.created, attach.edited, attach.display_path, + attach.allow_cid, attach.allow_gid, attach.deny_cid, attach.deny_gid from attach + where folder = '%s' and uid = %d $perms $suffix", + dbesc($folder), + intval($channel_id) + ); + } foreach ($r as $rr) { if(\App::$module === 'cloud' && (strpos($rr['filename'],'.') === 0) && (! get_pconfig($channel_id,'system','show_dot_files')) ) continue; // @FIXME I don't think we use revisions currently in attach structures. - // In case we see any in the wild provide a unique filename. This + // In case we see any in the wild provide a unique filename. This // name may or may not be accessible if($rr['revision']) @@ -710,13 +738,12 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo //logger('filename: ' . $rr['filename'], LOGGER_DEBUG); if (intval($rr['is_dir'])) { - $ret[] = new Directory($path . '/' . $rr['filename'], $auth); + $ret[] = new Directory($path . '/' . $rr['filename'], $rr, $auth); } else { $ret[] = new File($path . '/' . $rr['filename'], $rr, $auth); } } - return $ret; } @@ -738,15 +765,14 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo return $ret; } - $r = q("SELECT channel_id, channel_address, profile.publish FROM channel left join profile on profile.uid = channel.channel_id WHERE channel_removed = 0 AND channel_system = 0 AND (channel_pageflags & %d) = 0", + $r = q("SELECT channel_id, channel_address, profile.publish FROM channel left join profile on profile.uid = channel.channel_id WHERE channel_removed = 0 AND channel_system = 0 AND (channel_pageflags & %d) = 0 and profile.is_default = 1", intval(PAGE_HIDDEN) ); - if ($r) { foreach ($r as $rr) { - if (perm_is_allowed($rr['channel_id'], $auth->observer, 'view_storage') && $rr['publish']) { + if ((perm_is_allowed($rr['channel_id'], $auth->observer, 'view_storage') && $rr['publish'])|| $rr['channel_id'] == $this->auth->channel_id) { logger('found channel: /cloud/' . $rr['channel_address'], LOGGER_DATA); - $ret[] = new Directory($rr['channel_address'], $auth); + $ret[] = new Directory($rr['channel_address'], [], $auth); } } } @@ -778,7 +804,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo } if ((! $file) || ($file === '/')) { - return new Directory('/', $auth); + return new Directory('/', [], $auth); } $file = trim($file, '/'); @@ -848,7 +874,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo if ($test) return true; // final component was a directory. - return new Directory($file, $auth); + return new Directory($file, [], $auth); } if ($errors) { @@ -867,7 +893,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo return true; if (intval($r[0]['is_dir'])) { - return new Directory($path . '/' . $r[0]['filename'], $auth); + return new Directory($path . '/' . $r[0]['filename'], [], $auth); } else { return new File($path . '/' . $r[0]['filename'], $r[0], $auth); @@ -888,7 +914,7 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo $used = 0; $limit = 0; $free = 0; - + if ($this->auth->owner_id) { $channel = channelx_by_n($this->auth->owner_id); if($channel) { @@ -919,5 +945,4 @@ class Directory extends DAV\Node implements DAV\ICollection, DAV\IQuota, DAV\IMo return [ (int) $used, (int) $free ]; } - } diff --git a/Zotlabs/Widget/Categories.php b/Zotlabs/Widget/Categories.php index 82c37cd0c..94ad469da 100644 --- a/Zotlabs/Widget/Categories.php +++ b/Zotlabs/Widget/Categories.php @@ -21,7 +21,9 @@ class Categories { if(($articles) && (! Apps::system_app_installed(App::$profile['profile_uid'],'Articles'))) return ''; - if((! App::$profile['profile_uid']) + $files = ((array_key_exists('files',$arr) && $arr['files']) ? true : false); + + if((! App::$profile['profile_uid']) || (! perm_is_allowed(App::$profile['profile_uid'],get_observer_hash(),(($cards || $articles) ? 'view_pages' : 'view_stream')))) { return ''; } @@ -29,12 +31,14 @@ class Categories { $cat = ((x($_REQUEST,'cat')) ? htmlspecialchars($_REQUEST['cat'],ENT_COMPAT,'UTF-8') : ''); $srchurl = (($cards) ? App::$argv[0] . '/' . App::$argv[1] : App::$query_string); $srchurl = rtrim(preg_replace('/cat\=[^\&].*?(\&|$)/is','',$srchurl),'&'); - $srchurl = str_replace(array('?f=','&f='),array('',''),$srchurl); + $srchurl = str_replace(array('?f=','&f=', '/?'),array('', '', ''),$srchurl); if($cards) return cardcategories_widget($srchurl, $cat); elseif($articles) return articlecategories_widget($srchurl, $cat); + elseif($files) + return filecategories_widget($srchurl, $cat); else return categories_widget($srchurl, $cat); @@ -433,7 +433,7 @@ define ( 'TERM_FORUM', 11 ); define ( 'TERM_EMOJI', 12 ); define ( 'TERM_OBJ_POST', 1 ); -define ( 'TERM_OBJ_PHOTO', 2 ); +define ( 'TERM_OBJ_FILE', 2 ); define ( 'TERM_OBJ_PROFILE', 3 ); define ( 'TERM_OBJ_CHANNEL', 4 ); define ( 'TERM_OBJ_OBJECT', 5 ); diff --git a/include/attach.php b/include/attach.php index c9649a4ce..69ccceaf6 100644 --- a/include/attach.php +++ b/include/attach.php @@ -647,12 +647,12 @@ function attach_store($channel, $observer_hash, $options = '', $arr = null) { $pathname = filepath_macro($newalbum); } elseif(array_key_exists('folder',$arr)) { - $x = q("select filename from attach where hash = '%s' and uid = %d limit 1", + $x = q("select display_path from attach where hash = '%s' and uid = %d limit 1", dbesc($arr['folder']), intval($channel['channel_id']) ); if($x) - $pathname = $x[0]['filename']; + $pathname = $x[0]['display_path']; } else { $pathname = filepath_macro($album); @@ -1534,7 +1534,7 @@ function attach_drop_photo($channel_id,$resource) { $interactive = (($x[0]['item_hidden']) ? false : true); drop_item($x[0]['id'], $interactive, $stage); } - + $r = q("SELECT content FROM photo WHERE resource_id = '%s' AND uid = %d AND os_storage = 1", dbesc($resource), intval($channel_id) @@ -1544,7 +1544,7 @@ function attach_drop_photo($channel_id,$resource) { @unlink(dbunescbin($i['content'])); } } - + q("DELETE FROM photo WHERE uid = %d AND resource_id = '%s'", intval($channel_id), dbesc($resource) @@ -2495,7 +2495,7 @@ function copy_folder_to_cloudfiles($channel, $observer_hash, $srcpath, $cloudpat return true; } /** - * This function performs an in place directory-to-directory move of a stored attachment or photo. + * This function performs an in place directory-to-directory move of a stored resource. * The data is physically moved in the store/nickname storage location and the paths adjusted * in the attach structure (and if applicable the photo table). The new 'album name' is recorded * for photos and will show up immediately there. @@ -2507,60 +2507,64 @@ function copy_folder_to_cloudfiles($channel, $observer_hash, $srcpath, $cloudpat * @param int $channel_id * @param int $resource_id * @param string $new_folder_hash - * @return void|boolean + * @param (optional) string $newname + * @param (optional) boolean $recurse + * @return array Associative array with: + * * \e boolean \b success + * * \e string \b resource_id */ -function attach_move($channel_id, $resource_id, $new_folder_hash) { +function attach_move($channel_id, $resource_id, $new_folder_hash, $newname = '', $recurse = true) { + + $ret = [ + 'success' => false, + 'resource_id' => $resource_id + ]; $c = channelx_by_n($channel_id); if(! ($c && $resource_id)) - return false; - + return $ret; // find the resource to be moved - $r = q("select * from attach where hash = '%s' and uid = %d limit 1", dbesc($resource_id), intval($channel_id) ); if(! $r) { logger('resource_id not found'); - return false; + return $ret; } $oldstorepath = dbunescbin($r[0]['content']); - // find the resource we are moving to - if($new_folder_hash) { $n = q("select * from attach where hash = '%s' and uid = %d and is_dir = 1 limit 1", dbesc($new_folder_hash), intval($channel_id) ); if(! $n) - return false; + return $ret; + $newdirname = $n[0]['filename']; $newalbumname = $n[0]['display_path']; $newstorepath = dbunescbin($n[0]['content']) . '/' . $resource_id; } else { - // root directory - + $newdirname = EMPTY_STR; $newalbumname = EMPTY_STR; $newstorepath = 'store/' . $c['channel_address'] . '/' . $resource_id; } - rename($oldstorepath,$newstorepath); - - // duplicate detection. If 'overwrite' is specified, return false because we can't yet do that. - - $filename = $r[0]['filename']; - - // don't do duplicate check unless our parent folder has changed. + if ($recurse) { + rename($oldstorepath,$newstorepath); + } - if($r[0]['folder'] !== $new_folder_hash) { + $oldfilename = $r[0]['filename']; + $filename = (($newname) ? basename($newname) : $oldfilename); + // duplicate detection. + if($recurse) { $s = q("select filename, id, hash, filesize from attach where filename = '%s' and folder = '%s' ", dbesc($filename), dbesc($new_folder_hash) @@ -2568,9 +2572,10 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { if($s) { $overwrite = get_pconfig($channel_id,'system','overwrite_dup_files'); + // If 'overwrite' is specified, return false because we can't yet do that. if($overwrite) { /// @fixme - return; + return $ret; } else { if(strpos($filename,'.') !== false) { @@ -2586,7 +2591,8 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { if(preg_match('/(.*?)\([0-9]{1,}\)$/',$basename,$matches)) $basename = $matches[1]; - $v = q("select filename from attach where ( filename = '%s' OR filename like '%s' ) and folder = '%s' ", + $v = q("select filename from attach where uid = %d and ( filename = '%s' OR filename like '%s' ) and folder = '%s' ", + intval($channel_id), dbesc($basename . $ext), dbesc($basename . '(%)' . $ext), dbesc($new_folder_hash) @@ -2609,12 +2615,14 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { while($found); $filename = $basename . '(' . $x . ')' . $ext; } - else + else { $filename = $basename . $ext; + } } } } + q("update attach set content = '%s', folder = '%s', filename = '%s' where id = %d", dbescbin($newstorepath), dbesc($new_folder_hash), @@ -2631,7 +2639,6 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { intval($r[0]['id']) ); - if($r[0]['is_photo']) { q("update photo set album = '%s', filename = '%s', os_path = '%s', display_path = '%s' where resource_id = '%s' and uid = %d", @@ -2643,11 +2650,24 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { intval($channel_id) ); - q("update photo set content = '%s' where resource_id = '%s' and uid = %d and imgscale = 0", + q("update photo set content = CASE imgscale WHEN 0 THEN '%s' ELSE CONCAT('%s', '-', imgscale) END where resource_id = '%s' and uid = %d and os_storage = 1", + dbescbin($newstorepath), dbescbin($newstorepath), dbesc($resource_id), intval($channel_id) ); + + // now rename the thumbnails in os_storage - the original should have been copied before already + $ps = q("SELECT content, imgscale FROM photo WHERE uid = %d AND resource_id = '%s' and imgscale > 0 and os_storage = 1", + intval($channel_id), + dbesc($resource_id) + ); + + if ($recurse) { + foreach($ps as $p) { + rename($oldstorepath . '-' . $p['imgscale'], $p['content']); + } + } } if($r[0]['is_dir']) { @@ -2658,19 +2678,223 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { ); if($x) { foreach($x as $xv) { - $rs = attach_move($channel_id,$xv['hash'],$r[0]['hash']); - if(! $rs) { + $rs = attach_move($channel_id, $xv['hash'], $r[0]['hash'], '', false); + if(! $rs['success']) { $move_success = false; break; } } } - return $move_success; + + $ret['success'] = $move_success; + return $ret; } - return true; + $ret['success'] = true; + return $ret; } +/** + * This function performs an in place directory-to-directory copy of a stored resource. + * The data is physically copyed in the store/nickname storage location and the paths adjusted + * in the attach structure (and if applicable the photo table). The new 'album name' is recorded + * for photos and will show up immediately there. + * This takes a channel_id, attach.hash of the file to copy (this is the same as a photo resource_id), and + * the attach.hash of the new parent folder, which must already exist. If $new_folder_hash is blank or empty, + * the new file is copyed to the root of the channel's storage area. + * + * + * @param int $channel_id + * @param int $resource_id + * @param string $new_folder_hash + * @param (optional) string $newname + * @param (optional) boolean $recurse + * @return array Associative array with: + * * \e boolean \b success + * * \e string \b resource_id of the new resource + */ +function attach_copy($channel_id, $resource_id, $new_folder_hash, $newname = '', $recurse = true) { + + $ret = [ + 'success' => false, + 'resource_id' => '' + ]; + + $c = channelx_by_n($channel_id); + if(! ($c && $resource_id)) + return $ret; + + // find the resource to be copied + $r = q("select * from attach where hash = '%s' and uid = %d limit 1", + dbesc($resource_id), + intval($channel_id) + ); + if(! $r) { + logger('resource_id not found'); + return $ret; + } + + $a = $r[0]; + $new_resource_id = new_uuid(); + + $ret['resource_id'] = $new_resource_id; + + $oldstorepath = dbunescbin($r[0]['content']); + + // find the resource we are copying to + if($new_folder_hash) { + $n = q("select * from attach where hash = '%s' and uid = %d and is_dir = 1 limit 1", + dbesc($new_folder_hash), + intval($channel_id) + ); + if(! $n) { + logger('new_folder_hash not found'); + return $ret; + } + + $newdirname = $n[0]['filename']; + $newalbumname = $n[0]['display_path']; + $newstorepath = dbunescbin($n[0]['content']) . '/' . $new_resource_id; + } + else { + // root directory + $newdirname = EMPTY_STR; + $newalbumname = EMPTY_STR; + $newstorepath = 'store/' . $c['channel_address'] . '/' . $new_resource_id; + } + + if(is_dir($oldstorepath)) { + os_mkdir($newstorepath,STORAGE_DEFAULT_PERMISSIONS,true); + } + else { + copy($oldstorepath,$newstorepath); + } + + $oldfilename = $r[0]['filename']; + $filename = (($newname) ? basename($newname) : $oldfilename); + + // duplicate detection. If 'overwrite' is specified, return false because we can't yet do that. + if($recurse) { + $s = q("select filename, id, hash, filesize from attach where filename = '%s' and folder = '%s' ", + dbesc($filename), + dbesc($new_folder_hash) + ); + + if($s) { + $overwrite = get_pconfig($channel_id,'system','overwrite_dup_files'); + if($overwrite) { + /// @fixme + return $ret; + } + else { + if(strpos($filename,'.') !== false) { + $basename = substr($filename,0,strrpos($filename,'.')); + $ext = substr($filename,strrpos($filename,'.')); + } + else { + $basename = $filename; + $ext = ''; + } + + $matches = false; + if(preg_match('/(.*?)\([0-9]{1,}\)$/',$basename,$matches)) + $basename = $matches[1]; + + $v = q("select filename from attach where uid = %d and ( filename = '%s' OR filename like '%s' ) and folder = '%s' ", + intval($channel_id), + dbesc($basename . $ext), + dbesc($basename . '(%)' . $ext), + dbesc($new_folder_hash) + ); + + if($v) { + $x = 1; + + do { + $found = false; + foreach($v as $vv) { + if($vv['filename'] === $basename . '(' . $x . ')' . $ext) { + $found = true; + break; + } + } + if($found) + $x++; + } + while($found); + $filename = $basename . '(' . $x . ')' . $ext; + } + else { + $filename = $basename . $ext; + } + } + } + } + + unset($a['id']); + $a['hash'] = $new_resource_id; + $a['content'] = $newstorepath; + $a['folder'] = $new_folder_hash; + $a['filename'] = $filename; + + create_table_from_array('attach', $a, ['content']); + + $x = attach_syspaths($channel_id, $new_resource_id); + + q("update attach set os_path = '%s', display_path = '%s' where hash = '%s'", + dbesc($x['os_path']), + dbesc($x['path']), + dbesc($new_resource_id) + ); + + if($a['is_photo']) { + + $ps = q("SELECT * FROM photo WHERE uid = %d AND resource_id = '%s'", + intval($channel_id), + dbesc($resource_id) + ); + + foreach($ps as $p) { + unset($p['id']); + $p['resource_id'] = $new_resource_id; + $p['album'] = $newalbumname; + $p['filename'] = $filename; + $p['os_path'] = $x['os_path']; + $p['display_path'] = $x['path']; + if($p['os_storage']) { + $p['content'] = (($p['imgscale'] == 0) ? $newstorepath : $newstorepath . '-' . $p['imgscale']); + + //the original should have been copied before already + if($p['imgscale'] > 0) + copy($oldstorepath, $p['content']); + } + create_table_from_array('photo', $p, ['content']); + } + } + + if($r[0]['is_dir']) { + $copy_success = true; + $x = q("select hash from attach where folder = '%s' and uid = %d", + dbesc($r[0]['hash']), + intval($channel_id) + ); + if($x) { + foreach($x as $xv) { + $rs = attach_copy($channel_id,$xv['hash'],$new_resource_id, '', false); + if(! $rs['success']) { + $copy_success = false; + break; + } + } + } + + $ret['success'] = $copy_success; + return $ret; + } + + $ret['success'] = true; + return $ret; +} /** * Used to generate a select input box of all your folders diff --git a/include/connections.php b/include/connections.php index 658fb6ee6..100e595d0 100644 --- a/include/connections.php +++ b/include/connections.php @@ -71,7 +71,7 @@ function abook_connections($channel_id, $sql_conditions = '') { intval($channel_id) ); return(($r) ? $r : array()); -} +} function abook_self($channel_id) { $r = q("select * from abook left join xchan on abook_xchan = xchan_hash where abook_channel = %d @@ -79,7 +79,7 @@ function abook_self($channel_id) { intval($channel_id) ); return(($r) ? $r[0] : array()); -} +} function vcard_from_xchan($xchan, $observer = null, $mode = '') { @@ -119,14 +119,15 @@ function vcard_from_xchan($xchan, $observer = null, $mode = '') { if(array_key_exists('channel_id',$xchan)) App::$profile_uid = $xchan['channel_id']; - $url = (($observer) - ? z_root() . '/magic?f=&owa=1&bdest=' . bin2hex($xchan['xchan_url']) . '&addr=' . $xchan['xchan_addr'] + $url = (($observer) + ? z_root() . '/magic?f=&owa=1&bdest=' . bin2hex($xchan['xchan_url']) . '&addr=' . $xchan['xchan_addr'] : $xchan['xchan_url'] ); - + return replace_macros(get_markup_template('xchan_vcard.tpl'),array( '$name' => $xchan['xchan_name'], - '$photo' => ((is_array(App::$profile) && array_key_exists('photo',App::$profile)) ? App::$profile['photo'] : $xchan['xchan_photo_l']), + '$addr' => (($xchan['xchan_addr']) ? $xchan['xchan_addr'] : $xchan['xchan_url']), + '$photo' => $xchan['xchan_photo_m'], '$follow' => (($xchan['xchan_addr']) ? $xchan['xchan_addr'] : $xchan['xchan_url']), '$link' => zid($xchan['xchan_url']), '$connect' => $connect, @@ -177,10 +178,10 @@ function abook_toggle_flag($abook,$flag) { ); - // if unsetting the archive bit, update the timestamps so we'll try to connect for an additional 30 days. + // if unsetting the archive bit, update the timestamps so we'll try to connect for an additional 30 days. if(($flag === ABOOK_FLAG_ARCHIVED) && (intval($abook['abook_archived']))) { - $r = q("update abook set abook_connected = '%s', abook_updated = '%s' + $r = q("update abook set abook_connected = '%s', abook_updated = '%s' where abook_id = %d and abook_channel = %d", dbesc(datetime_convert()), dbesc(datetime_convert()), @@ -210,7 +211,7 @@ function mark_orphan_hubsxchans() { if($dirmode == DIRECTORY_MODE_NORMAL) return; - $r = q("update hubloc set hubloc_error = 1 where hubloc_error = 0 + $r = q("update hubloc set hubloc_error = 1 where hubloc_error = 0 and hubloc_network = 'zot' and hubloc_connected < %s - interval %s", db_utcnow(), db_quoteinterval('36 day') ); @@ -301,9 +302,9 @@ function remove_all_xchan_resources($xchan, $channel_id = 0) { ); // Cannot delete just one side of the conversation since we do not allow - // you to block private mail replies. This would leave open a gateway for abuse. + // you to block private mail replies. This would leave open a gateway for abuse. // Both participants are owners of the conversation and both can remove it. - + $r = q("delete from mail where ( from_xchan = '%s' or to_xchan = '%s' )", dbesc($xchan), dbesc($xchan) @@ -387,7 +388,7 @@ function contact_remove($channel_id, $abook_id) { $already_saved = []; foreach($r as $rr) { $w = $x = $y = null; - + // optimise so we only process newly seen parent items if (in_array($rr['parent'],$already_saved)) { continue; @@ -423,7 +424,7 @@ function contact_remove($channel_id, $abook_id) { drop_item($rr['id'],false); } } - + q("delete from abook where abook_id = %d and abook_channel = %d", intval($abook['abook_id']), intval($channel_id) @@ -501,17 +502,17 @@ function update_vcard($arr,$vcard = null) { $fn = $arr['fn']; - + // This isn't strictly correct and could be a cause for concern. // 'N' => array_reverse(explode(' ', $fn)) - // What we really want is + // What we really want is // 'N' => Adams;John;Quincy;Reverend,Dr.;III // which is a very difficult parsing problem especially if you allow // the surname to contain spaces. The only way to be sure to get it - // right is to provide a form to input all the various fields and not - // try to extract it from the FN. + // right is to provide a form to input all the various fields and not + // try to extract it from the FN. if(! $vcard) { $vcard = new \Sabre\VObject\Component\VCard([ @@ -689,12 +690,12 @@ function get_vcard_array($vc,$id) { if(is_array($entry['address'])) { array_walk($entry['address'],'array_escape_tags'); } - else { + else { $entry['address'] = (string) escape_tags($entry['address']); } $adrs[] = $entry; - + } } @@ -768,7 +769,7 @@ function vcard_query(&$r) { if($a) { foreach($a as $av) { for($x = 0; $x < count($r); $x ++) { - if($r[$x]['abook_xchan'] == $av['xchan']) { + if($r[$x]['abook_xchan'] == $av['xchan']) { $vctmp = \Sabre\VObject\Reader::read($av['v']); $r[$x]['vcard'] = (($vctmp) ? get_vcard_array($vctmp,$r[$x]['abook_id']) : [] ); } diff --git a/include/contact_widgets.php b/include/contact_widgets.php index 626a825b2..3b22a3c6d 100644 --- a/include/contact_widgets.php +++ b/include/contact_widgets.php @@ -93,7 +93,7 @@ function categories_widget($baseurl,$selected = '') { dbesc(ACTIVITY_UPDATE) ); } - else + else $r = unserialize($content); $terms = array(); @@ -206,6 +206,40 @@ function articlecategories_widget($baseurl,$selected = '') { return ''; } +function filecategories_widget($baseurl,$selected = '') { + + $perms = permissions_sql(App::$profile['profile_uid']); + + $terms = array(); + $r = q("select distinct(term.term) + from term join attach on term.oid = attach.id + where attach.uid = %d + and term.uid = attach.uid + and term.ttype = %d + and term.otype = %d + $perms + order by term.term asc", + intval(App::$profile['profile_uid']), + intval(TERM_CATEGORY), + intval(TERM_OBJ_FILE) + ); + + if($r && count($r)) { + foreach($r as $rr) + $terms[] = array('name' => $rr['term'], 'selected' => (($selected == $rr['term']) ? 'selected' : '')); + + return replace_macros(get_markup_template('categories_widget.tpl'),array( + '$title' => t('Categories'), + '$desc' => '', + '$sel_all' => (($selected == '') ? 'selected' : ''), + '$all' => t('Everything'), + '$terms' => $terms, + '$base' => $baseurl, + )); + } + + return ''; +} function common_friends_visitor_widget($profile_uid,$cnt = 25) { diff --git a/include/feedutils.php b/include/feedutils.php index 5f5f563f8..352b8f038 100644 --- a/include/feedutils.php +++ b/include/feedutils.php @@ -14,9 +14,6 @@ * @return string with an atom feed */ -use Ramsey\Uuid\Uuid; -use Ramsey\Uuid\Exception\UnsatisfiedDependencyException; - function get_public_feed($channel, $params) { if(! $params) @@ -435,13 +432,7 @@ function get_atom_elements($feed, $item) { $res['plink'] = unxmlify($item->get_link(0)); $res['item_rss'] = 1; - try { - $uuid = Uuid::uuid5(Uuid::NAMESPACE_URL, $res['plink'])->toString(); - } catch (UnsatisfiedDependencyException $e) { - $uuid = md5($res['plink']); - } - - $res['uuid'] = $uuid; + $res['uuid'] = uuid_from_url($res['plink']); $summary = unxmlify($item->get_description(true)); diff --git a/include/items.php b/include/items.php index 956b259af..bcdc6c687 100755 --- a/include/items.php +++ b/include/items.php @@ -4072,6 +4072,11 @@ function delete_item_lowlevel($item, $stage = DROPITEM_NORMAL) { if($stage == DROPITEM_PHASE1) return true; + $r = q("delete from term where otype = %d and oid = %d", + intval(TERM_OBJ_POST), + intval($item['id']) + ); + q("delete from iconfig where iid = %d", intval($item['id']) ); diff --git a/include/text.php b/include/text.php index c2a45814c..3fb17b648 100644 --- a/include/text.php +++ b/include/text.php @@ -2983,7 +2983,7 @@ function handle_tag(&$body, &$str_tags, $profile_uid, $tag, $in_network = true) $str_tags .= $newtag; } } - + $fn_results[] = [ 'replaced' => $replaced, @@ -3060,7 +3060,7 @@ function handle_tag(&$body, &$str_tags, $profile_uid, $tag, $in_network = true) ]; } } - + return $fn_results; } @@ -3098,7 +3098,7 @@ function linkify_tags(&$body, $uid, $in_network = true) { function getIconFromType($type) { $iconMap = array( //Folder - t('Collection') => 'fa-folder-o', + 'Collection' => 'fa-folder-o', 'multipart/mixed' => 'fa-folder-o', //dirs in attach use this mime type //Common file 'application/octet-stream' => 'fa-file-o', @@ -3242,7 +3242,7 @@ function item_url_replace($channel,&$item,$old,$new,$oldnick = '') { if($oldnick) json_url_replace('/' . $oldnick . '/' ,'/' . $channel['channel_address'] . '/' ,$item['target']); } - + $item['body'] = preg_replace("/(\[zrl=".preg_quote($old,'/')."\/(photo|photos|gallery)\/".$channel['channel_address'].".+\]\[zmg=\d+x\d+\])".preg_quote($old,'/')."\/(.+\[\/zmg\])/", '${1}'.$new.'/${3}', $item['body']); $item['body'] = preg_replace("/".preg_quote($old,'/')."\/(search|\w+\/".$channel['channel_address'].")/", $new.'/${1}', $item['body']); @@ -3575,7 +3575,7 @@ function cleanup_bbcode($body) { $body = preg_replace_callback('/\[\$b64url(.*?)\[\/(url)\]/ism','\red_unescape_codeblock',$body); $body = preg_replace_callback('/\[\$b64code(.*?)\[\/(code)\]/ism','\red_unescape_codeblock',$body); $body = preg_replace_callback('/\[\$b64svg(.*?)\[\/(svg)\]/ism','\red_unescape_codeblock',$body); - + // fix any img tags that should be zmg $body = preg_replace_callback('/\[img(.*?)\](.*?)\[\/img\]/ism','\red_zrlify_img_callback',$body); @@ -3791,7 +3791,7 @@ function array_path_exists($str,$arr) { /** - * @brief Generate a unique ID. + * @brief Generate a random v4 UUID. * * @return string */ @@ -3807,6 +3807,22 @@ function new_uuid() { } +/** + * @brief Generate a name-based v5 UUID in the URL namespace + * + * @param string $url + * @return string + */ +function uuid_from_url($url) { + + try { + $hash = Uuid::uuid5(Uuid::NAMESPACE_URL, $url)->toString(); + } catch (UnsatisfiedDependencyException $e) { + $hash = md5($url); + } + return $hash; +} + function svg2bb($s) { $s = preg_replace("/\<text (.*?)\>(.*?)\<(.*?)\<\/text\>/", '<text $1>$2<$3</text>', $s); diff --git a/view/css/mod_cloud.css b/view/css/mod_cloud.css index 83deddf8a..8dce7f180 100644 --- a/view/css/mod_cloud.css +++ b/view/css/mod_cloud.css @@ -1,10 +1,14 @@ #files-mkdir-tools, #files-upload-tools, -[id^="perms-panel-"] { +.cloud-tool, +.cloud-multi-tool, +#multi-dbtn-acl, +#multi-dropdown-button { display: none; } -[id^="perms-panel-"] { +.attach-edit-panel, +#attach-multi-edit-panel { padding: 3px 10px 0px 10px !important; } @@ -17,28 +21,40 @@ width: 100%; } -#cloud-index td:nth-child(1){ +#cloud-index td:nth-child(3) a { + display: block; +} + +#cloud-index td:nth-child(1) { padding: 7px 3px 7px 10px; } -#cloud-index td:nth-child(2){ +#cloud-index td:nth-child(6) { + padding: 7px 10px 7px 3px; +} + +#cloud-index td:nth-child(3) { word-break: break-all; } -#cloud-index th:nth-child(8), -#cloud-index td:nth-child(8){ +#cloud-index td:nth-child(4) { + white-space: nowrap; +} + +#cloud-index th:nth-child(7), +#cloud-index td:nth-child(7) { padding: 7px 3px; white-space: nowrap; } -#cloud-index th:nth-child(9), -#cloud-index td:nth-child(9){ - padding: 7px 10px 7px 7px; +#cloud-index th:nth-child(8), +#cloud-index td:nth-child(8) { + padding: 7px 10px; white-space: nowrap; } .cloud-index-tool { - padding: 7px 10px; + padding: 7px 0px; } #files-upload { @@ -52,9 +68,39 @@ box-shadow: inset 0 0px 7px #5cb85c; } +.attach-drop-ok { + background-color: aliceblue !important; +} + +.attach-drop-zone { + border-top-width: 3px; + border-top-style: dashed; + border-top-color: #eee; + border-bottom-width: 3px; + border-bottom-style: dashed; + border-bottom-color: #eee; +} + .upload-progress-bar { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOM2RFTDwAE2QHxFMHIIwAAAABJRU5ErkJggg==') repeat-y; background-size: 0px; padding: 0px !important; height: 3px; } + +.bootstrap-tagsinput input[type=text] { + height: unset; +} + +.bootstrap-tagsinput { + box-shadow: none; +} + +.breadcrumb { + padding: 0px 10px 0px 10px; + margin: 0px 0px 3px 0px; +} + +.breadcrumb-item { + margin: 0px; +} diff --git a/view/js/acl.js b/view/js/acl.js index 940fdaa44..ee6cb062f 100644 --- a/view/js/acl.js +++ b/view/js/acl.js @@ -30,7 +30,7 @@ function ACL(backend_url) { that.custom = $("#acl-custom"); that.acl_select = $("#acl-select"); - // set the initial ACL lists in case the enclosing form gets submitted before the ajax loader completes. + // set the initial ACL lists in case the enclosing form gets submitted before the ajax loader completes. //that.on_submit(); /*events*/ @@ -90,7 +90,7 @@ ACL.prototype.get_form_data = function(event) { } -// no longer called only on submit - call to update whenever a change occurs to the acl list. +// no longer called only on submit - call to update whenever a change occurs to the acl list. ACL.prototype.on_submit = function() { $('.acl-field').remove(); diff --git a/view/js/main.js b/view/js/main.js index ca82e3101..7df705603 100644 --- a/view/js/main.js +++ b/view/js/main.js @@ -44,7 +44,7 @@ if(localStorage.getItem('uid') !== localUser.toString()) { window.onstorage = function(e) { if(e.key === 'uid' && parseInt(e.newValue) !== localUser) { if(window_needs_alert) { - window_needs_alert = false; + window_needs_alert = false; alert("Your identity has changed. Page reload required!"); window.location.reload(); return; @@ -99,12 +99,12 @@ $(document).ready(function() { wordSeparator : aStr['t16'], numbers : aStr['t17'], }; - + jQuery.timeago.settings.allowFuture = true; if(sse_enabled) { if(typeof(window.SharedWorker) === 'undefined') { - // notifications with multiple tabs open will not work very well in this scenario + // notifications with multiple tabs open will not work very well in this scenario var evtSource = new EventSource('/sse'); evtSource.addEventListener('notifications', function(e) { @@ -298,7 +298,7 @@ function handle_comment_form(e) { $('#' + commentElm).addClass('expanded').removeAttr('placeholder'); $('#' + commentElm).attr('tabindex','9'); $('#' + submitElm).attr('tabindex','10'); - + form.find(':not(:visible)').show(); } @@ -452,7 +452,7 @@ function insertCommentAttach(comment,id) { $('body').css('cursor', 'wait'); $('#invisible-comment-upload').trigger('click'); - + return false; } @@ -631,7 +631,7 @@ function updateConvItems(mode,data) { if(mode === 'append') { next = 'threads-end'; } - + if(mode === 'replace') { $('.thread-parent').remove(); // clear existing content } @@ -652,7 +652,7 @@ function updateConvItems(mode,data) { if($('#collapsed-comments-'+itmId).is(':visible')) isVisible = true; - // insert the content according to the mode and first_page + // insert the content according to the mode and first_page // and whether or not the content exists already (overwrite it) if($('#' + ident).length == 0) { @@ -765,7 +765,7 @@ function updateConvItems(mode,data) { } // Setup to determine if the media player is playing. This affects - // some content loading decisions. + // some content loading decisions. $('video').off('playing'); $('video').off('pause'); @@ -1253,24 +1253,25 @@ function dopin(id) { } function dropItem(url, object) { + var confirm = confirmDelete(); + if(confirm) { + var id = url.split('/')[2]; + $('body').css('cursor', 'wait'); + $(object + ', #pinned-wrapper-' + id).css('opacity', 0.33); - var confirm = confirmDelete(); - if(confirm) { - var id = url.split('/')[2]; - $('body').css('cursor', 'wait'); - $(object + ', #pinned-wrapper-' + id).fadeTo('fast', 0.33, function () { - $.get(url).done(function() { - $(object + ', #pinned-wrapper-' + id).remove(); - $('body').css('cursor', 'auto'); - }); - }); - if($('#wall-item-pinned-' + id).length) - $.post('pin/pin', { 'id' : id }); - return true; - } - else { - return false; + $.get(url, function() { + $(object + ', #pinned-wrapper-' + id).remove(); + $('body').css('cursor', 'auto'); + }); + + if($('#wall-item-pinned-' + id).length) + $.post('pin/pin', { 'id' : id }); + + return true; } + else { + return false; + } } function dosubthread(ident) { @@ -1339,18 +1340,6 @@ function lockview(type, id) { }); } -function filestorage(event, nick, id) { - $('#cloud-index-' + last_filestorage_id).removeClass('cloud-index-active'); - $('#perms-panel-' + last_filestorage_id).hide().html(''); - $('#file-edit-' + id).show(); - $.get('filestorage/' + nick + '/' + id + '/edit', function(data) { - $('#cloud-index-' + id).addClass('cloud-index-active'); - $('#perms-panel-' + id).html(data).show(); - $('#file-edit-' + id).hide(); - last_filestorage_id = id; - }); -} - function submitPoll(id) { $.post('vote/' + id, @@ -1473,17 +1462,17 @@ function preview_mail() { } function bin2hex(s) { - // Converts the binary representation of data to hex - // - // version: 812.316 - // discuss at: http://phpjs.org/functions/bin2hex - // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + bugfixed by: Onno Marsman - // + bugfixed by: Linuxworld - // * example 1: bin2hex('Kev'); - // * returns 1: '4b6576' - // * example 2: bin2hex(String.fromCharCode(0x00)); - // * returns 2: '00' + // Converts the binary representation of data to hex + // + // version: 812.316 + // discuss at: http://phpjs.org/functions/bin2hex + // + original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // + bugfixed by: Onno Marsman + // + bugfixed by: Linuxworld + // * example 1: bin2hex('Kev'); + // * returns 1: '4b6576' + // * example 2: bin2hex(String.fromCharCode(0x00)); + // * returns 2: '00' var v,i, f = 0, a = []; s += ''; f = s.length; diff --git a/view/js/mod_cloud.js b/view/js/mod_cloud.js index 031895caf..7f2bdfab7 100644 --- a/view/js/mod_cloud.js +++ b/view/js/mod_cloud.js @@ -3,22 +3,341 @@ */ $(document).ready(function () { + // call initialization file if (window.File && window.FileList && window.FileReader) { UploadInit(); } + + var attach_drop_id; + var attach_draging; + + // Per File Tools + + $('.cloud-tool-perms-btn').on('click', function (e) { + e.preventDefault(); + let id = $(this).data('id'); + activate_id(id); + }); + + $('.cloud-tool-rename-btn').on('click', function (e) { + e.preventDefault(); + let id = $(this).data('id'); + activate_id(id); + $('#cloud-tool-rename-' + id).show(); + }); + + $('.cloud-tool-move-btn').on('click', function (e) { + e.preventDefault(); + let id = $(this).data('id'); + activate_id(id); + $('#cloud-tool-move-' + id).show(); + }); + + $('.cloud-tool-categories-btn').on('click', function (e) { + e.preventDefault(); + let id = $(this).data('id'); + activate_id(id); + $('#id_categories_' + id).tagsinput({ + tagClass: 'badge badge-pill badge-warning text-dark' + }); + $('#cloud-tool-categories-' + id).show(); + }); + + $('.cloud-tool-download-btn').on('click', function (e) { + close_and_deactivate_all_panels(); + }); + + $('.cloud-tool-delete-btn').on('click', function (e) { + e.preventDefault(); + let id = $(this).data('id'); + + close_and_deactivate_all_panels(); + + let confirm = confirmDelete(); + if (confirm) { + $('body').css('cursor', 'wait'); + $('#cloud-index-' + id).css('opacity', 0.33); + + let form = $('#attach_edit_form_' + id).serializeArray(); + form.push({name: 'delete', value: 1}); + + $.post('attach_edit', form, function (data) { + if (data.success) { + $('#cloud-index-' + id + ', #cloud-tools-' + id).remove(); + $('body').css('cursor', 'auto'); + } + return true; + }); + + } + return false; + }); + + $('.cloud-tool-cancel-btn').on('click', function (e) { + e.preventDefault(); + let id = $(this).data('id'); + close_and_deactivate_all_panels(); + $('#attach_edit_form_' + id).trigger('reset'); + $('#id_categories_' + id).tagsinput('destroy'); + }); + + // Per File Tools Eend + + // DnD + + $(document).on('drop', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + + $(document).on('dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + + $(document).on('dragleave', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + + $('.cloud-index.attach-drop').on('drop', function (e) { + + let target = $(this); + let folder = target.data('folder'); + let id = target.data('id'); + + + if(typeof folder === typeof undefined) { + return false; + } + + // Check if it's a file + if (e.dataTransfer.files[0]) { + $('#file-folder').val(folder); + return true; + } + + if(id === attach_drop_id) { + return false; + } + + if(target.hasClass('attach-drop-zone') && attach_draging) { + return false; + } + + target.removeClass('attach-drop-ok'); + + $.post('attach_edit', {'channel_id': channelId, 'dnd': 1, 'attach_id': attach_drop_id, ['newfolder_' + attach_drop_id]: folder }, function (data) { + if (data.success) { + $('#cloud-index-' + attach_drop_id + ', #cloud-tools-' + attach_drop_id).remove(); + attach_drop_id = null; + } + }); + }); + + $('.cloud-index.attach-drop').on('dragover', function (e) { + let target = $(this); + + if(target.hasClass('attach-drop-zone') && attach_draging) { + return false; + } + + target.addClass('attach-drop-ok'); + }); + + $('.cloud-index').on('dragleave', function (e) { + let target = $(this); + target.removeClass('attach-drop-ok'); + }); + + $('.cloud-index').on('dragstart', function (e) { + let target = $(this); + attach_drop_id = target.data('id'); + // dragstart is not fired if a file is draged onto the window + // we use this to distinguish between drags and file drops + attach_draging = true; + }); + + $('.cloud-index').on('dragend', function (e) { + let target = $(this); + target.removeClass('attach-drop-ok'); + attach_draging = false; + }); + + // DnD End + + // Multi Tools + + $('#cloud-multi-tool-select-all').on('change', function (e) { + if ($(this).is(':checked')) { + $('.cloud-multi-tool-checkbox').prop('checked', true); + $('.cloud-index:not(#cloud-index-up)').addClass('cloud-index-selected cloud-index-active'); + $('.cloud-tools').addClass('cloud-index-selected'); + } + else { + $('.cloud-multi-tool-checkbox').prop('checked', false); + $('.cloud-index').removeClass('cloud-index-selected cloud-index-active'); + $('.cloud-tools').removeClass('cloud-index-selected'); + } + + $('.cloud-multi-tool-checkbox').trigger('change'); + }); + + + $('.cloud-multi-tool-checkbox').on('change', function (e) { + let id = $(this).val(); + + if ($(this).is(':checked')) { + $('#cloud-index-' + id).addClass('cloud-index-selected cloud-index-active'); + $('#cloud-tools-' + id).addClass('cloud-index-selected'); + $('<input id="aid_' + id + '" class="attach-ids-input" type="hidden" name="attach_ids[]" value="' + id + '">').prependTo('#attach_multi_edit_form'); + } + else { + $('#cloud-index-' + id).removeClass('cloud-index-selected cloud-index-active'); + $('#cloud-tools-' + id).removeClass('cloud-index-selected'); + if ($('#cloud-multi-tool-select-all').is(':checked')) + $('#cloud-multi-tool-select-all').prop('checked', false); + + $('#aid_' + id).remove(); + } + + if($('.cloud-multi-tool-checkbox:checked').length) { + close_all_panels(); + $('#cloud-multi-actions').addClass('bg-warning'); + $('#multi-dropdown-button').fadeIn(); + } + else { + $('#cloud-multi-actions').removeClass('bg-warning'); + $('#multi-dropdown-button').fadeOut(); + close_and_deactivate_all_panels(); + disable_multi_acl(); + } + + }); + + $('#cloud-multi-tool-perms-btn').on('click', function (e) { + e.preventDefault(); + + close_all_panels(); + enable_multi_acl(); + + $('#cloud-multi-tool-submit').show(); + }); + + $('#cloud-multi-tool-move-btn').on('click', function (e) { + e.preventDefault(); + + close_all_panels(); + disable_multi_acl(); + + $('#cloud-multi-tool-submit, #cloud-multi-tool-move').show(); + }); + + $('#cloud-multi-tool-categories-btn').on('click', function (e) { + e.preventDefault(); + + close_all_panels(); + disable_multi_acl(); + + $('#id_categories').tagsinput({ + tagClass: 'badge badge-pill badge-warning text-dark' + }); + + $('#cloud-multi-tool-submit, #cloud-multi-tool-categories').show(); + }); + + $('#cloud-multi-tool-delete-btn').on('click', function (e) { + e.preventDefault(); + + let post_data = $('.cloud-multi-tool-checkbox').serializeArray(); + + if(! post_data.length) { + return false; + } + let confirm = confirmDelete(); + if (confirm) { + $('body').css('cursor', 'wait'); + $('.cloud-index-selected').css('opacity', 0.33); + + post_data.push( + { name: 'channel_id', value: channelId }, + { name: 'delete', value: 1}, + ); + + $.post('attach_edit', post_data, function (data) { + if (data.success) { + console.log(data); + $('.cloud-index-selected').remove(); + $('body').css('cursor', 'auto'); + } + return true; + }); + } + return false; + + }); + + $('.cloud-multi-tool-cancel-btn').on('click', function (e) { + e.preventDefault(); + + close_and_deactivate_all_panels(); + disable_multi_acl(); + + $('#attach_multi_edit_form').trigger('reset'); + $('#id_categories').tagsinput('destroy'); + }); + + // Multi Tools End + + // Helper Functions + + function disable_multi_acl() { + $('#multi-perms').val(0); + $('#multi-dbtn-acl, #recurse_container').hide(); + $('#attach-multi-edit-perms').removeClass('btn-group'); + } + + function enable_multi_acl() { + $('#multi-perms').val(1); + $('#multi-dbtn-acl, #recurse_container').show(); + $('#attach-multi-edit-perms').addClass('btn-group'); + } + + function close_all_panels() { + $('.cloud-tool, .cloud-multi-tool').hide(); + } + + function deactivate_all_panels() { + $('.cloud-index').removeClass('cloud-index-active'); + } + + function close_and_deactivate_all_panels() { + close_all_panels(); + deactivate_all_panels(); + } + + function activate_id(id) { + close_and_deactivate_all_panels(); + $('#cloud-multi-tool-select-all, .cloud-multi-tool-checkbox').prop('checked', false).trigger('change'); + + $('#cloud-tool-submit-' + id).show(); + $('#cloud-index-' + id).addClass('cloud-index-active'); + $('#cloud-tool-submit-' + id).show(); + } + }); -// + + + // initialize function UploadInit() { - var fileselect = $("#files-upload"); - var filedrag = $("#cloud-drag-area"); var submit = $("#upload-submit"); var count = 1; + var filedrag = $(".cloud-index.attach-drop"); - $('#invisible-cloud-file-upload').fileupload({ url: 'file_upload', dataType: 'json', @@ -26,8 +345,8 @@ function UploadInit() { maxChunkSize: 4 * 1024 * 1024, add: function(e,data) { - $(data.files).each( function() { this.count = ++ count; prepareHtml(this); }); - + $(data.files).each( function() { this.count = ++ count; prepareHtml(this); }); + var allow_cid = ($('#ajax-upload-files').data('allow_cid') || []); var allow_gid = ($('#ajax-upload-files').data('allow_gid') || []); var deny_cid = ($('#ajax-upload-files').data('deny_cid') || []); @@ -49,7 +368,6 @@ function UploadInit() { }); data.formData = $('#ajax-upload-files').serializeArray(); - data.submit(); }, @@ -58,7 +376,7 @@ function UploadInit() { // there will only be one file, the one we are looking for - $(data.files).each( function() { + $(data.files).each( function() { var idx = this.count; // Dynamically update the percentage complete displayed in the file upload list @@ -70,7 +388,6 @@ function UploadInit() { }, - stop: function(e,data) { window.location.href = window.location.href; } @@ -81,60 +398,17 @@ function UploadInit() { } -// file drag hover -function DragDropUploadFileHover(e) { - e.stopPropagation(); - e.preventDefault(); - e.currentTarget.className = (e.type == "dragover" ? "hover" : ""); -} - -// file selection via drag/drop -function DragDropUploadFileSelectHandler(e) { - // cancel event and hover styling - DragDropUploadFileHover(e); - - // fetch FileList object - var files = e.target.files || e.originalEvent.dataTransfer.files; - - $('.new-upload').remove(); - - // process all File objects - for (var i = 0, f; f = files[i]; i++) { - prepareHtml(f, i); - UploadFile(f, i); - } -} - -// file selection via input -function UploadFileSelectHandler(e) { - // fetch FileList object - if(e.target.id === 'upload-submit') { - e.preventDefault(); - var files = e.data[0].files; - } - if(e.target.id === 'files-upload') { - $('.new-upload').remove(); - var files = e.target.files; - } - // process all File objects - for (var i = 0, f; f = files[i]; i++) { - if(e.target.id === 'files-upload') - prepareHtml(f, i); - if(e.target.id === 'upload-submit') { - UploadFile(f, i); - } - } -} function prepareHtml(f) { var num = f.count - 1; var i = f.count; $('#cloud-index #new-upload-progress-bar-' + num.toString()).after( '<tr id="new-upload-' + i + '" class="new-upload">' + + '<td></td>' + '<td><i class="fa ' + getIconFromType(f.type) + '" title="' + f.type + '"></i></td>' + '<td>' + f.name + '</td>' + - '<td id="upload-progress-' + i + '"></td><td></td><td></td><td></td><td></td>' + + '<td id="upload-progress-' + i + '"></td><td></td><td></td>' + '<td class="d-none d-md-table-cell">' + formatSizeUnits(f.size) + '</td><td class="d-none d-md-table-cell"></td>' + '</tr>' + '<tr id="new-upload-progress-bar-' + i + '" class="new-upload">' + @@ -199,63 +473,4 @@ function getIconFromType(type) { return iconFromType; } -// upload files -function UploadFile(file, idx) { - - - window.filesToUpload = window.filesToUpload + 1; - - var xhr = new XMLHttpRequest(); - xhr.withCredentials = true; // Include the SESSION cookie info for authentication - - (xhr.upload || xhr).addEventListener('progress', function (e) { - - var done = e.position || e.loaded; - var total = e.totalSize || e.total; - // Dynamically update the percentage complete displayed in the file upload list - $('#upload-progress-' + idx).html(Math.round(done / total * 100) + '%'); - $('#upload-progress-bar-' + idx).css('background-size', Math.round(done / total * 100) + '%'); - - if(done == total) { - $('#upload-progress-' + idx).html('Processing...'); - } - - }); - - - xhr.addEventListener('load', function (e) { - //we could possibly turn the filenames to real links here and add the delete and edit buttons to avoid page reload... - $('#upload-progress-' + idx).html('Ready!'); - - //console.log('xhr upload complete', e); - window.fileUploadsCompleted = window.fileUploadsCompleted + 1; - - // When all the uploads have completed, refresh the page - if (window.filesToUpload > 0 && window.fileUploadsCompleted === window.filesToUpload) { - - window.fileUploadsCompleted = window.filesToUpload = 0; - - // After uploads complete, refresh browser window to display new files - window.location.href = window.location.href; - } - }); - - - xhr.addEventListener('error', function (e) { - $('#upload-progress-' + idx).html('<span style="color: red;">ERROR</span>'); - }); - - // POST to the entire cloud path -// xhr.open('post', 'file_upload', true); - -// var formfields = $("#ajax-upload-files").serializeArray(); - -// var data = new FormData(); -// $.each(formfields, function(i, field) { -// data.append(field.name, field.value); -// }); -// data.append('userfile', file); - -// xhr.send(data); -} diff --git a/view/pdl/mod_cloud.pdl b/view/pdl/mod_cloud.pdl index a5461df50..44ef1e4fc 100644 --- a/view/pdl/mod_cloud.pdl +++ b/view/pdl/mod_cloud.pdl @@ -1,5 +1,6 @@ [region=aside] [widget=vcard][/widget] +[widget=categories][var=files]1[/var][/widget] [/region] [region=right_aside] [widget=notifications][/widget] diff --git a/view/theme/redbasic/css/style.css b/view/theme/redbasic/css/style.css index 545e610b5..7e7f33d8e 100644 --- a/view/theme/redbasic/css/style.css +++ b/view/theme/redbasic/css/style.css @@ -365,7 +365,7 @@ footer { bottom:1px; text-align: right; padding-bottom: 1em; - padding-right: 3em; + padding-right: 3em; } .birthday-today, @@ -484,7 +484,7 @@ footer { .pager_next, .pager-prev, .pager-next, -.pager_n { +.pager_n { border: 1px solid #ccc; background: transparent; padding: 4px; @@ -738,7 +738,7 @@ nav .acpopup { height: auto; overflow: auto; border-bottom: 2px solid #cccccc; padding-bottom: 1em; - margin-bottom: 1em; + margin-bottom: 1em; } .oauthapp img { float: left; @@ -1016,7 +1016,7 @@ th,td { max-width: 19.4em; overflow: hidden; } - + /* mail */ img.mail-conv-sender-photo { @@ -1560,12 +1560,12 @@ blockquote { margin-top:-3px; } -dl.bb-dl > dt { +dl.bb-dl > dt { /* overriding the default dl style from bootstrap, as bootstrap's style of a bold unindented line followed by a plain unindented line is already acheivable in bbcode without dl */ - font-weight: normal; -} + font-weight: normal; +} dl.dl-terms-monospace > dt { font-family: monospace; } dl.dl-terms-bold > dt { font-weight: bold; } dl.dl-terms-italic > dt { font-style: italic; } @@ -1576,7 +1576,7 @@ dl.bb-dl:not(.dl-horizontal) > dd { margin-left: 2em; } dl.bb-dl > dd > li { - /* adding some indent so bullet-list items will line up better with + /* adding some indent so bullet-list items will line up better with dl descriptions if someone wants to be impure and combine them */ margin-left: 1em; } @@ -1815,3 +1815,7 @@ dl.bb-dl > dd > li { span.default-highlight { background-color: yellow; } + +.bootstrap-tagsinput { + width: 100%; +} diff --git a/view/tpl/breadcrumb.tpl b/view/tpl/breadcrumb.tpl new file mode 100644 index 000000000..205b712d9 --- /dev/null +++ b/view/tpl/breadcrumb.tpl @@ -0,0 +1,11 @@ +<nav aria-label="breadcrumb"> + <ol class="breadcrumb bg-transparent"> + {{foreach $breadcrumbs as $breadcrumb}} + {{if $breadcrumb@last}} + <li class="breadcrumb-item active h3 pt-3 pb-3" aria-current="page">{{$breadcrumb.name}}</li> + {{else}} + <li class="breadcrumb-item h3 cloud-index attach-drop pt-3 pb-3" data-folder="{{$breadcrumb.hash}}" title="{{$breadcrumb.hash}}"><a href="{{$breadcrumb.path}}">{{$breadcrumb.name}}</a></li> + {{/if}} + {{/foreach}} + </ol> +</nav> diff --git a/view/tpl/categories_widget.tpl b/view/tpl/categories_widget.tpl index 1341c652c..fd27dca44 100755 --- a/view/tpl/categories_widget.tpl +++ b/view/tpl/categories_widget.tpl @@ -1,12 +1,12 @@ <div id="categories-sidebar" class="widget"> <h3>{{$title}}</h3> <div id="categories-sidebar-desc">{{$desc}}</div> - + <ul class="nav nav-pills flex-column"> <li class="nav-item"><a href="{{$base}}" class="nav-link{{if $sel_all}} active{{/if}}">{{$all}}</a></li> {{foreach $terms as $term}} - <li class="nav-item"><a href="{{$base}}?f=&cat={{$term.name|urlencode}}" class="nav-link{{if $term.selected}} active{{/if}}">{{$term.name}}</a></li> + <li class="nav-item"><a href="{{$base}}/?cat={{$term.name|urlencode}}" class="nav-link{{if $term.selected}} active{{/if}}">{{$term.name}}</a></li> {{/foreach}} </ul> - + </div> diff --git a/view/tpl/cloud_actionspanel.tpl b/view/tpl/cloud_actionspanel.tpl index 292452cca..65319379f 100644 --- a/view/tpl/cloud_actionspanel.tpl +++ b/view/tpl/cloud_actionspanel.tpl @@ -19,15 +19,16 @@ </form> <div class="clear"></div> </div> -<div id="files-upload-tools" class="section-content-tools-wrapper"> +<div id="files-upload-tools" class="section-content-tools-wrapper "> {{if $quota.limit || $quota.used}}<div class="{{if $quota.warning}}section-content-danger-wrapper{{else}}section-content-info-wrapper{{/if}}">{{if $quota.warning}}<strong>{{$quota.warning}} </strong>{{/if}}{{if $quota.desc}}{{$quota.desc}}<br><br>{{/if}}</div>{{/if}} <form id="ajax-upload-files" method="post" action="#" enctype="multipart/form-data" class="acl-form" data-form_id="ajax-upload-files" data-allow_cid='{{$allow_cid}}' data-allow_gid='{{$allow_gid}}' data-deny_cid='{{$deny_cid}}' data-deny_gid='{{$deny_gid}}'> - <input type="hidden" name="directory" value="{{$path}}" /> + <input id="file-folder"type="hidden" name="folder" value="{{$folder}}" /> <input type="hidden" name="channick" value="{{$channick}}" /> <input type="hidden" name="return_url" value="{{$return_url}}" /> - <!--label for="files-upload">{{$upload_header}}</label> - <input class="form-group pull-left" id="files-upload" type="file" name="files[]" multiple --> {{include file="field_checkbox.tpl" field=$notify}} + <div class="cloud-index attach-drop attach-drop-zone text-center p-4 mb-3" data-folder="{{$folder}}"> + <span class="text-muted">You can select files via the upload button or drop them right here or into an existing folder.</span> + </div> <div class="pull-right btn-group"> <div class="btn-group"> {{if $lockstate}} @@ -41,4 +42,10 @@ </form> <div class="clear"></div> </div> +{{if $breadcrumbs_html}} {{$aclselect}} +{{/if}} +{{if $breadcrumbs_html}} +{{$breadcrumbs_html}} +<hr class="m-0"> +{{/if}} diff --git a/view/tpl/cloud_directory.tpl b/view/tpl/cloud_directory.tpl index 90347d274..618de418f 100644 --- a/view/tpl/cloud_directory.tpl +++ b/view/tpl/cloud_directory.tpl @@ -1,84 +1,258 @@ -<div id="cloud-drag-area" class="section-content-wrapper-np"> -{{if $tiles}} +<div class="section-content-wrapper-np"> + {{if $tiles}} <table id="cloud-index"> <tr id="new-upload-progress-bar-1"></tr> {{* this is needed to append the upload files in the right order *}} </table> {{if $parentpath}} <div class="cloud-container" > - - <div class="cloud-icon tiles"><a href="{{$parentpath.path}}"> - <div class="cloud-icon-container"><i class="fa fa-fw fa-level-up" ></i></div> - </a> - </div> - <div class="cloud-title"><a href="{{$parentpath.path}}">..</a> - </div> + <div class="cloud-icon tiles"> + <a href="{{$parentpath}}"> + <div class="cloud-icon-container"> + <i class="fa fa-fw fa-level-up" ></i> + </div> + </a> + </div> + <div class="cloud-title"> + <a href="{{$parentpath}}">..</a> + </div> </div> {{/if}} {{foreach $entries as $item}} <div class="cloud-container"> - <div class="cloud-icon tiles"><a href="{{$item.fullPath}}"> - {{if $item.photo_icon}} - <img src="{{$item.photo_icon}}" title="{{$item.type}}" > - {{else}} - <div class="cloud-icon-container"><i class="fa fa-fw {{$item.iconFromType}}" title="{{$item.type}}"></i></div> - {{/if}} - </a> - </div> - <div class="cloud-title"><a href="{{$item.fullPath}}"> - {{$item.displayName}} - </a> - </div> - {{if $item.is_owner}} - - {{/if}} + <div class="cloud-icon tiles"><a href="{{$item.rel_path}}"> + {{if $item.photo_icon}} + <img src="{{$item.photo_icon}}" title="{{$item.type}}" > + {{else}} + <div class="cloud-icon-container"> + <i class="fa fa-fw {{$item.icon_from_type}}" title="{{$item.type}}"></i> + </div> + {{/if}} + </div> + <div class="cloud-title"> + <a href="{{$item.rel_path}}"> + {{$item.name}} + </a> + </div> + {{if $item.is_owner}} + {{* add file tools here*}} + {{/if}} </div> {{/foreach}} <div class="clear"></div> -{{else}} + {{else}} <table id="cloud-index"> <tr> - <th width="1%"></th> - <th width="92%">{{$name}}</th> - <th width="1%"></th><th width="1%"></th><th width="1%"></th><th width="1%"></th> - <th width="1%">{{*{{$type}}*}}</th> + <th width="1%">{{* multi tool checkbox *}}</th> + <th width="1%">{{* icon *}}</th> + <th width="93%">{{$name}}</th> + <th width="1%">{{* categories *}}</th> + <th width="1%">{{* lock icon *}}</th> + <th width="1%">{{* tools icon *}}</th> <th width="1%" class="d-none d-md-table-cell">{{$size}}</th> <th width="1%" class="d-none d-md-table-cell">{{$lastmod}}</th> </tr> - {{if $parentpath}} - <tr> - <td><i class="fa fa-level-up"></i>{{*{{$parentpath.icon}}*}}</td> - <td><a href="{{$parentpath.path}}" title="{{$parent}}">..</a></td> - <td></td><td></td><td></td><td></td> - <td>{{*[{{$parent}}]*}}</td> - <td class="d-none d-md-table-cell"></td> - <td class="d-none d-md-table-cell"></td> + {{if $parentpath}} + <tr id="cloud-index-up" class="cloud-index{{if ! $is_root_folder}} attach-drop{{/if}}"{{if ! $is_root_folder}} data-folder="{{$folder_parent}}"/{{/if}}> + <td></td> + <td><i class="fa fa-level-up"></i></td> + <td colspan="6"><a href="{{$parentpath}}" title="{{$parent}}" class="p-2" draggable="false">..</a></td> </tr> - {{/if}} + <tr class="cloud-tools"> + <td colspan="8" class="attach-edit-panel">{{* this is for display consistency *}}</td> + </tr> + {{/if}} + {{if $channel_id && $is_owner && $entries.0}} + <tr id="cloud-multi-actions"> + <td colspan="2"> + <div class="form-check form-check-inline"> + <input class="form-check-input" type="checkbox" id="cloud-multi-tool-select-all" value="" title="Select all"> + </div> + </td> + <td colspan="3"> + <div class="form-check form-check-inline"> + <label class="form-check-label" for="cloud-multi-tool-select-all">Select all</label> + </div> + </td> + <td colspan="3"> + {{if $is_owner}} + <div class="dropdown"> + <button class="btn btn-warning btn-sm" id="multi-dropdown-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-fw fa-ellipsis-v d-table-cell"></i><span class="d-none d-md-table-cell">Bulk Actions</span> + </button> + <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-button"> + {{if $is_owner}} + <a id="cloud-multi-tool-perms-btn" class="dropdown-item" href="#"><i class="fa fa-fw fa-lock"></i> Adjust permissions</a> + {{/if}} + <a id="cloud-multi-tool-move-btn" class="dropdown-item" href="#"><i class="fa fa-fw fa-copy"></i> Move or copy</a> + <a id="cloud-multi-tool-categories-btn" class="dropdown-item" href="#"><i class="fa fa-fw fa-asterisk"></i> Categories</a> + <a id="cloud-multi-tool-delete-btn" class="dropdown-item" href="#"><i class="fa fa-fw fa-trash-o"></i> {{$delete}}</a> + </div> + </div> + {{else if $is_admin}} + <div class="dropdown"> + <button class="btn btn-warning btn-sm" id="multi-dropdown-button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-fw fa-ellipsis-v d-table-cell"></i><span class="d-none d-md-table-cell">Bulk Actions</span> + </button> + <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-button"> + <a id="cloud-multi-tool-delete-btn" class="dropdown-item" href="#"><i class="fa fa-fw fa-trash-o"></i> {{$admin_delete}}</a> + </div> + </div> + {{/if}} + </td> + </tr> + <tr id="cloud-multi-tools"> + <td id="attach-multi-edit-panel" colspan="8"> + <form id="attach_multi_edit_form" action="attach_edit" method="post" class="acl-form" data-form_id="attach_multi_edit_form" data-allow_cid='{{$allow_cid}}' data-allow_gid='{{$allow_gid}}' data-deny_cid='{{$deny_cid}}' data-deny_gid='{{$deny_gid}}'> + <input type="hidden" name="channel_id" value="{{$channel_id}}" /> + <input id="multi-perms" type="hidden" name="permissions" value="0"> + <input type="hidden" name="return_path" value="{{$return_path}}"> + <div id="cloud-multi-tool-move" class="cloud-multi-tool"> + {{include file="field_select.tpl" field=$newfolder}} + {{include file="field_checkbox.tpl" field=$copy}} + </div> + <div id="cloud-multi-tool-categories" class="cloud-multi-tool"> + {{include file="field_input.tpl" field=$categories}} + </div> + <div id="cloud-multi-tool-submit" class="cloud-multi-tool"> + {{if $is_owner}} + {{include file="field_checkbox.tpl" field=$recurse}} + {{/if}} + <div id="attach-multi-submit" class="form-group"> + <button id="cloud-multi-tool-cancel-btn" class="btn btn-outline-secondary btn-sm cloud-multi-tool-cancel-btn" type="button"> + Cancel + </button> + <div id="attach-multi-edit-perms" class="btn-group float-right"> + {{if $is_owner}} + <button id="multi-dbtn-acl" class="btn btn-outline-secondary btn-sm" data-toggle="modal" data-target="#aclModal" title="{{$permset}}" type="button"> + <i id="multi-jot-perms-icon" class="fa fa-{{$lockstate}} jot-icons jot-perms-icon"></i> + </button> + {{/if}} + <button id="multi-dbtn-submit" class="btn btn-primary btn-sm" type="submit" name="submit"> + {{$edit}} + </button> + </div> + </div> + </div> + </form> + </td> + </tr> + {{/if}} <tr id="new-upload-progress-bar-1"></tr> {{* this is needed to append the upload files in the right order *}} - {{foreach $entries as $item}} - <tr id="cloud-index-{{$item.attachId}}"> - <td><i class="fa {{$item.iconFromType}}" title="{{$item.type}}"></i></td> - <td><a href="{{$item.fullPath}}">{{$item.displayName}}</a></td> - {{if $item.is_owner}} - <td class="cloud-index-tool">{{$item.attachIcon}}</td> - <td class="cloud-index-tool"><div id="file-edit-{{$item.attachId}}" class="spinner-wrapper"><div class="spinner s"></div></div></td> - <td class="cloud-index-tool"><i class="fakelink fa fa-pencil" onclick="filestorage(event, '{{$nick}}', {{$item.attachId}});"></i></td> - <td class="cloud-index-tool"><a href="#" title="{{$delete}}" onclick="dropItem('{{$item.fileStorageUrl}}/{{$item.attachId}}/delete/json', '#cloud-index-{{$item.attachId}},#cloud-tools-{{$item.attachId}}'); return false;"><i class="fa fa-trash-o drop-icons"></i></a></td> - - {{else}} - <td></td><td></td><td></td>{{if $is_admin || $item.is_creator}}<td class="cloud-index-tool"><a href="#" title="{{if $is_admin}}{{$admin_delete}}{{else}}{{$delete}}{{/if}}" onclick="dropItem('{{$item.fileStorageUrl}}/{{$item.attachId}}/delete/json', '#cloud-index-{{$item.attachId}},#cloud-tools-{{$item.attachId}}'); return false;"><i class="fa fa-trash-o drop-icons"></i></a>{{else}}<td>{{/if}}</td> - {{/if}} - <td>{{*{{$item.type}}*}}</td> - <td class="d-none d-md-table-cell">{{$item.sizeFormatted}}</td> - <td class="d-none d-md-table-cell">{{$item.lastmodified}}</td> + {{foreach $entries as $item}} + <tr id="cloud-index-{{$item.attach_id}}" class="cloud-index{{if $item.collection}} attach-drop{{/if}}"{{if $item.collection}} data-folder="{{$item.resource}}"{{/if}} data-id="{{$item.attach_id}}" draggable="true"> + <td> + {{if $channel_id && $is_owner}} + <div class="form-check form-check-inline"> + <input class="form-check-input cloud-multi-tool-checkbox" type="checkbox" id="cloud-multi-tool-checkbox-{{$item.attach_id}}" name="attach_ids[]" value="{{$item.attach_id}}"> + </div> + {{/if}} + </td> + <td><i class="fa {{$item.icon_from_type}}" title="{{$item.type}}"></i></td> + <td><a href="{{$item.rel_path}}" class="p-2" draggable="false">{{$item.name}}</a></td> + <td>{{$item.terms}}</td> + <td class="cloud-index-tool p-2">{{if $item.lockstate == 'lock'}}<i class="fa fa-fw fa-{{$item.lockstate}}"></i>{{/if}}</td> + <td class="cloud-index-tool"> + {{if ($item.is_owner || $item.is_creator) && $item.attach_id}} + <div class="dropdown"> + <button class="btn btn-link btn-sm" id="dropdown-button-{{$item.attach_id}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-fw fa-ellipsis-v"></i> + </button> + <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-button-{{$item.attach_id}}"> + {{if $item.is_owner}} + <a id="cloud-tool-perms-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-perms-btn" href="#" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-{{$item.lockstate}}"></i> Adjust permissions</a> + {{/if}} + <a id="cloud-tool-rename-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-rename-btn" href="#" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-pencil"></i> Rename</a> + <a id="cloud-tool-move-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-move-btn" href="#" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-copy"></i> Move or copy</a> + <a id="cloud-tool-categories-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-categories-btn" href="#" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-asterisk"></i> Categories</a> + {{if !$item.collection}} + {{if $item.is_owner}} + <a id="cloud-tool-share-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-share-btn" href="/rpost?attachment=[attachment]{{$item.resource}},{{$item.revision}}[/attachment]&acl[allow_cid]={{$item.raw_allow_cid}}&acl[allow_gid]={{$item.raw_allow_gid}}&acl[deny_cid]={{$item.raw_deny_cid}}&acl[deny_gid]={{$item.raw_deny_gid}}" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-share-square-o"></i> Post</a> + {{/if}} + <a id="cloud-tool-download-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-download-btn" href="/attach/{{$item.resource}}" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-cloud-download"></i> Download</a> + {{/if}} + <a id="cloud-tool-delete-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-delete-btn" href="#" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-trash-o"></i> {{$delete}}</a> + </div> + </div> + {{else}} + {{if ($is_admin || !$item.collection) && $item.attach_id}} + <div class="dropdown"> + <button class="btn btn-link btn-sm" id="dropdown-button-{{$item.attach_id}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + <i class="fa fa-fw fa-ellipsis-v"></i> + </button> + <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdown-button-{{$item.attach_id}}"> + {{if !$item.collection}} + <a id="cloud-tool-download-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-download-btn" href="/attach/{{$item.resource}}" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-cloud-download"></i> Download</a> + {{/if}} + {{if $is_admin}} + <a id="cloud-tool-delete-btn-{{$item.attach_id}}" class="dropdown-item cloud-tool-delete-btn" href="#" data-id="{{$item.attach_id}}"><i class="fa fa-fw fa-trash-o"></i> {{$admin_delete}}</a> + {{/if}} + </div> + </div> + {{/if}} + </td> + {{/if}} + <td class="d-none d-md-table-cell p-2">{{$item.size_formatted}}</td> + <td class="d-none d-md-table-cell p-2">{{$item.last_modified}}</td> </tr> - <tr id="cloud-tools-{{$item.attachId}}"> - <td id="perms-panel-{{$item.attachId}}" colspan="9"></td> + <tr id="cloud-tools-{{$item.attach_id}}" class="cloud-tools"> + <td id="attach-edit-panel-{{$item.attach_id}}" class="attach-edit-panel" colspan="8"> + <form id="attach_edit_form_{{$item.attach_id}}" action="attach_edit" method="post" class="acl-form" data-form_id="attach_edit_form_{{$item.attach_id}}" data-allow_cid='{{$item.allow_cid}}' data-allow_gid='{{$item.allow_gid}}' data-deny_cid='{{$item.deny_cid}}' data-deny_gid='{{$item.deny_gid}}'> + <input type="hidden" name="attach_id" value="{{$item.attach_id}}" /> + <input type="hidden" name="channel_id" value="{{$channel_id}}" /> + <input type="hidden" name="return_path" value="{{$return_path}}"> + <div id="cloud-tool-rename-{{$item.attach_id}}" class="cloud-tool"> + {{include file="field_input.tpl" field=$item.newfilename}} + </div> + <div id="cloud-tool-move-{{$item.attach_id}}" class="cloud-tool"> + {{include file="field_select.tpl" field=$item.newfolder}} + {{include file="field_checkbox.tpl" field=$item.copy}} + </div> + <div id="cloud-tool-categories-{{$item.attach_id}}" class="cloud-tool"> + {{include file="field_input.tpl" field=$item.categories}} + </div> + <div id="cloud-tool-submit-{{$item.attach_id}}" class="cloud-tool"> + {{if $item.is_owner}} + {{if !$item.collection}}{{include file="field_checkbox.tpl" field=$item.notify}}{{/if}} + {{if $item.collection}}{{include file="field_checkbox.tpl" field=$item.recurse}}{{/if}} + {{/if}} + <div id="attach-submit-{{$item.attach_id}}" class="form-group"> + <button id="cloud-tool-cancel-btn-{{$item.attach_id}}" class="btn btn-outline-secondary btn-sm cloud-tool-cancel-btn" type="button" data-id="{{$item.attach_id}}"> + Cancel + </button> + <div id="attach-edit-perms-{{$item.attach_id}}" class="btn-group float-right"> + {{if $item.is_owner}} + <button id="dbtn-acl-{{$item.attach_id}}" class="btn btn-outline-secondary btn-sm" data-toggle="modal" data-target="#aclModal" title="{{$permset}}" type="button"> + <i id="jot-perms-icon-{{$item.attach_id}}" class="fa fa-{{$item.lockstate}} jot-icons jot-perms-icon"></i> + </button> + {{/if}} + <button id="dbtn-submit-{{$item.attach_id}}" class="btn btn-primary btn-sm" type="submit" name="submit"> + {{$edit}} + </button> + </div> + </div> + </div> + <!--div id="cloud-tool-share-{{$item.attach_id}}" class=""> + <div id="attach-edit-tools-share-{{$item.attach_id}}" class="btn-group form-group"> + <button id="link-btn-{{$item.attach_id}}" class="btn btn-outline-secondary btn-sm" type="button" onclick="openClose('link-code-{{$item.attach_id}}');" title="{{$link_btn_title}}"> + <i class="fa fa-link jot-icons"></i> + </button> + </div> + </div> + {{if !$item.collection}} + <a href="/rpost?attachment=[attachment]{{$item.resource}},{{$item.revision}}[/attachment]" id="attach-btn" class="btn btn-outline-secondary btn-sm" title="{{$attach_btn_title}}"> + <i class="fa fa-share-square-o jot-icons"></i> + </a> + {{/if}} + <div id="link-code-{{$item.attach_id}}" class="form-group link-code"> + <label for="linkpasteinput-{{$item.attach_id}}">{{$cpldesc}}</label> + <input type="text" class="form-control" id="linkpasteinput-{{$item.attach_id}}" name="linkpasteinput-{{$item.attach_id}}" value="{{$item.full_path}}" onclick="this.select();"/> + </div--> + </form> + </td> </tr> - - {{/foreach}} + {{/foreach}} </table> {{/if}} </div> diff --git a/view/tpl/field_select.tpl b/view/tpl/field_select.tpl index 8c3776841..7cc624fab 100755 --- a/view/tpl/field_select.tpl +++ b/view/tpl/field_select.tpl @@ -1,4 +1,4 @@ - <div class="form-group"> + <div id="id_{{$field.0}}_wrapper" class="form-group"> <label for="id_{{$field.0}}">{{$field.1}}</label> <select class="form-control" name="{{$field.0}}" id="id_{{$field.0}}"> {{foreach $field.4 as $opt=>$val}}<option value="{{$opt}}" {{if $opt==$field.2}}selected="selected"{{/if}}>{{$val}}</option>{{/foreach}} diff --git a/view/tpl/xchan_vcard.tpl b/view/tpl/xchan_vcard.tpl index 9c357bba8..447839167 100755 --- a/view/tpl/xchan_vcard.tpl +++ b/view/tpl/xchan_vcard.tpl @@ -1,10 +1,25 @@ -<div id="vcard" class="vcard h-card"> -<div id="profile-photo-wrapper"><a href="{{$link}}"><img class="vcard-photo photo u-photo" src="{{$photo}}" alt="{{$name}}" /></a></div> -{{if $connect}} -<div class="connect-btn-wrapper"><a href="follow?f=&url={{$follow}}" class="btn btn-block btn-success btn-sm" rel="nofollow"><i class="fa fa-plus"></i> {{$connect}}</a></div> -{{/if}} -<div class="fn p-name">{{$name}}</div> +<div class="card mb-3 h-card"> + <div class="row"> + <div class="col-4"> + <a href="{{$link}}" > + <img class="u-photo" src="{{$photo}}" alt="{{$name}}" width="80px" height="80px"> + </a> + </div> + <div class="col m-1"> + <div class="row"> + <strong class="fn p-name">{{$name}}</strong> + </div> + <div class="row"> + <small class="text-muted p-adr">{{$addr}}</small> + </div> + {{if $connect}} + <div class="row mt-2"> + <a href="follow?f=&url={{$follow}}" class="btn btn-success btn-sm" rel="nofollow"> + <i class="fa fa-plus"></i> {{$connect}} + </a> + </div> + {{/if}} + </div> + </div> </div> - - |