From 4c872681798f86b19111778341ade240628490cc Mon Sep 17 00:00:00 2001 From: HWFord <54360213+HWFord@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:05:37 +0200 Subject: [PATCH 001/149] relates #2377 fix php warning in maintenence if the 3XL and 4xl size don't exist it creates a php warning. this avoids the warning because theses sizes haven't yet been generated so don't exist --- admin/themes/default/template/maintenance_actions.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/themes/default/template/maintenance_actions.tpl b/admin/themes/default/template/maintenance_actions.tpl index c9ffa4a22..63c070296 100644 --- a/admin/themes/default/template/maintenance_actions.tpl +++ b/admin/themes/default/template/maintenance_actions.tpl @@ -198,7 +198,7 @@ $(".delete-size-check").click( function () {
{foreach from=$purge_derivatives key=name item=url name=loop} -
+
{$name}
From 4f6da8ea6a906b7ab8e272f0fe45222437e3f850 Mon Sep 17 00:00:00 2001 From: Martin Raby Date: Tue, 22 Jul 2025 13:00:27 +0200 Subject: [PATCH 002/149] Relates #2351 : To see the newsletter promote, the account must have 2 weeks ancient, 3 albums created and 30 photos uploaded --- admin/intro.php | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/admin/intro.php b/admin/intro.php index c9629a5fe..356f4328e 100644 --- a/admin/intro.php +++ b/admin/intro.php @@ -100,13 +100,41 @@ fs_quick_check(); $template->set_filenames(array('intro' => 'intro.tpl')); if ($conf['show_newsletter_subscription'] and userprefs_get_param('show_newsletter_subscription', true)) { - $template->assign( - array( - 'EMAIL' => $user['email'], - 'SUBSCRIBE_BASE_URL' => get_newsletter_subscribe_base_url($user['language']), - 'OLD_NEWSLETTERS_URL' => get_old_newsletters_base_url($user['language']), - ) - ); + $query = ' + SELECT registration_date + FROM '.USER_INFOS_TABLE.' + WHERE registration_date IS NOT NULL + ORDER BY user_id ASC + LIMIT 1 + ;'; + list($register_date) = pwg_db_fetch_row(pwg_query($query)); + + $query = ' + SELECT COUNT(*) + FROM '.CATEGORIES_TABLE.' + ;'; + list($nb_cats) = pwg_db_fetch_row(pwg_query($query)); + + $query = ' + SELECT COUNT(*) + FROM '.IMAGES_TABLE.' + ;'; + list($nb_images) = pwg_db_fetch_row(pwg_query($query)); + + include_once(PHPWG_ROOT_PATH.'include/mdetect.php'); + $uagent_obj = new uagent_info(); + // To see the newsletter promote, the account must have 2 weeks ancient, 3 albums created and 30 photos uploaded + + if (!$uagent_obj->DetectIos() and strtotime($register_date) < strtotime('2 weeks ago') and $nb_cats >= 3 and $nb_images >= 30){ + $template->assign( + array( + 'EMAIL' => $user['email'], + 'SUBSCRIBE_BASE_URL' => get_newsletter_subscribe_base_url($user['language']), + 'OLD_NEWSLETTERS_URL' => get_old_newsletters_base_url($user['language']), + ) + ); + } + } From 5f0dc8548f7abe6385d9b8b59e1048bd7fadffdd Mon Sep 17 00:00:00 2001 From: Linty Date: Tue, 22 Jul 2025 18:26:01 +0200 Subject: [PATCH 003/149] issue #2355 update piwigo structure sql and fix api key bug after first install --- install.php | 1 + install/piwigo_structure-mysql.sql | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/install.php b/install.php index 738cd03c0..e4b029040 100644 --- a/install.php +++ b/install.php @@ -483,6 +483,7 @@ else // cache requires $logger which is not instanciated $user = build_user(1, false); log_user($user['id'], false); + $_SESSION['connected_with'] = 'pwg_ui'; $user['preferences']['show_whats_new_'.get_branch_from_version(PHPWG_VERSION)] = false; diff --git a/install/piwigo_structure-mysql.sql b/install/piwigo_structure-mysql.sql index ca47a8420..85f5b7364 100644 --- a/install/piwigo_structure-mysql.sql +++ b/install/piwigo_structure-mysql.sql @@ -410,10 +410,15 @@ DROP TABLE IF EXISTS `piwigo_user_auth_keys`; CREATE TABLE `piwigo_user_auth_keys` ( `auth_key_id` int(11) unsigned NOT NULL AUTO_INCREMENT, `auth_key` varchar(255) NOT NULL, + `apikey_secret` VARCHAR(255) DEFAULT NULL, `user_id` mediumint(8) unsigned NOT NULL, `created_on` datetime NOT NULL, `duration` int(11) unsigned DEFAULT NULL, `expired_on` datetime NOT NULL, + `apikey_name` VARCHAR(100) DEFAULT NULL, + `key_type` VARCHAR(40) DEFAULT NULL, + `revoked_on` datetime DEFAULT NULL, + `last_used_on` datetime DEFAULT NULL, PRIMARY KEY (`auth_key_id`) ) ENGINE=MyISAM; From fd54a917be23b65e1df3490bb995a9c5726fd587 Mon Sep 17 00:00:00 2001 From: plegall Date: Wed, 23 Jul 2025 17:24:52 +0200 Subject: [PATCH 004/149] hide whatsnew 16 for now --- admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin.php b/admin.php index 218afcc4d..f33b5f71d 100644 --- a/admin.php +++ b/admin.php @@ -394,7 +394,7 @@ $template->assign( 'WHATS_NEW_MAJOR_VERSION' => $whats_new_major_version, 'RELEASE_NOTE_URL' => $release_note_url, 'WHATS_NEW_IMGS' => $whats_new_imgs, - 'DISPLAY_BELL' => $display_bell, + 'DISPLAY_BELL' => false, // $display_bell, ) ); From bc4acec569ca5629deba162a9931a72d9adc77e1 Mon Sep 17 00:00:00 2001 From: plegall Date: Thu, 24 Jul 2025 21:29:38 +0200 Subject: [PATCH 005/149] issue #2390 warn about duplicate paths --- admin/include/functions.php | 24 ++++++++++++++++++++++++ language/en_UK/admin.lang.php | 1 + language/fr_FR/admin.lang.php | 1 + 3 files changed, 26 insertions(+) diff --git a/admin/include/functions.php b/admin/include/functions.php index 71b3c2706..654ce4515 100644 --- a/admin/include/functions.php +++ b/admin/include/functions.php @@ -3636,6 +3636,30 @@ SELECT return; } } + + // search for duplicate paths + $query = ' +SELECT + path + FROM '.IMAGES_TABLE.' + GROUP BY path + HAVING COUNT(*) > 1 +;'; + $duplicate_paths = query2array($query); + + if (count($duplicate_paths) > 0) + { + global $template; + + $template->assign( + 'header_msgs', + array( + l10n('We have found %d duplicate paths. Details provided by plugin Check Uploads', count($duplicate_paths)), + ) + ); + + return; + } } /** diff --git a/language/en_UK/admin.lang.php b/language/en_UK/admin.lang.php index b229ebc8b..5ab96daf5 100644 --- a/language/en_UK/admin.lang.php +++ b/language/en_UK/admin.lang.php @@ -1410,5 +1410,6 @@ $lang['Welcome to %s'] = 'Welcome to %s'; $lang['Save all photos'] = 'Save all photos'; $lang['Use standard Piwigo template for common pages.'] = 'Use standard Piwigo template for common pages.'; $lang['When enabled, a common template is used for the login, registration and forgotten password pages, regardless of the theme. Some themes might use these templates even if you uncheck this option'] = 'When enabled, a common template is used for the login, registration and forgotten password pages, regardless of the theme. Some themes might use these templates even if you uncheck this option'; +$lang['We have found %d duplicate paths. Details provided by plugin Check Uploads'] = 'We have found %d duplicate paths. Details provided by plugin Check Uploads'; // Leave this line empty \ No newline at end of file diff --git a/language/fr_FR/admin.lang.php b/language/fr_FR/admin.lang.php index 75144e438..2b0d5b94e 100644 --- a/language/fr_FR/admin.lang.php +++ b/language/fr_FR/admin.lang.php @@ -1412,4 +1412,5 @@ $lang['Welcome to %s'] = 'Bienvenue sur %s'; $lang['Save all photos'] = 'Enregistrer toutes les photos'; $lang['Use standard Piwigo template for common pages.'] = 'Utiliser le modèle standard de Piwigo pour les pages courantes.'; $lang['When enabled, a common template is used for the login, registration and forgotten password pages, regardless of the theme. Some themes might use these templates even if you uncheck this option'] = 'Lorsque cette option est activée, un modèle commun est utilisé pour les pages de connexion, d\'inscription et de mot de passe oublié, quel que soit le thème. Certains thèmes peuvent utiliser ces modèles même si cette option est décochée.'; +$lang['We have found %d duplicate paths. Details provided by plugin Check Uploads'] = 'Nous avons trouvé %d chemins anormalement dupliqués. À contrôler avec le plugin Check Uploads.'; // Leave this line empty From d0ac05d951fee342c05c11364279b015ddbd48f1 Mon Sep 17 00:00:00 2001 From: Perrom <107625315+Perrom@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:41:01 +0200 Subject: [PATCH 006/149] fixes #2353 update images with upload form (#2385) Add a mode to update photos with the upload form. Modify the upload formats form, so that it will update the photo with the same file extension. --- admin/include/functions_upload.inc.php | 34 +++- admin/photos_add_direct.php | 7 +- admin/themes/clear/theme.css | 10 ++ admin/themes/default/js/photos_add_direct.js | 151 ++++++++++++++++-- .../default/template/photos_add_direct.tpl | 59 +++++-- admin/themes/default/theme.css | 79 +++++++++ admin/themes/roma/theme.css | 15 +- include/ws_functions/pwg.images.php | 62 ++++++- ws.php | 6 +- 9 files changed, 385 insertions(+), 38 deletions(-) diff --git a/admin/include/functions_upload.inc.php b/admin/include/functions_upload.inc.php index 3ea729e1b..46e0fdaf9 100644 --- a/admin/include/functions_upload.inc.php +++ b/admin/include/functions_upload.inc.php @@ -520,8 +520,36 @@ SELECT 'filesize' => $file_infos['filesize'], ); - single_insert(IMAGE_FORMAT_TABLE, $insert); - $format_id = pwg_db_insert_id(IMAGE_FORMAT_TABLE); + + $query = ' +SELECT + format_id + FROM '.IMAGE_FORMAT_TABLE.' + WHERE image_id = '.$format_of.' + AND ext = "'.$format_ext.'" +;'; + + $formats = query2array($query); + if($formats) + { + $set_fields = array( + 'filesize' => $file_infos['filesize'], + ); + $where_fields = array( + 'format_id' => $formats[0]['format_id'], + 'image_id' => $format_of, + 'ext' => $format_ext, + ); + single_update(IMAGE_FORMAT_TABLE, $set_fields, $where_fields); + $format_id = $formats[0]['format_id']; + $add_status = "update"; + } + else + { + single_insert(IMAGE_FORMAT_TABLE, $insert); + $format_id = pwg_db_insert_id(IMAGE_FORMAT_TABLE); + $add_status = "add"; + } pwg_activity('photo', $format_of, 'edit', array('action'=>'add format', 'format_ext'=>$format_ext, 'format_id'=>$format_id)); @@ -530,7 +558,7 @@ SELECT trigger_notify('loc_end_add_format', $format_infos); - return $format_id; + return $add_status; } add_event_handler('upload_file', 'upload_file_pdf'); diff --git a/admin/photos_add_direct.php b/admin/photos_add_direct.php index e66fbbbc7..ea6cdd2f2 100644 --- a/admin/photos_add_direct.php +++ b/admin/photos_add_direct.php @@ -85,6 +85,7 @@ $display_formats = $conf['enable_formats'] && isset($_GET['formats']); $have_formats_original = false; $formats_original_info = array(); +$formats_ext_info = null; // If URL parameter isn't empty if ($display_formats && $_GET['formats']) @@ -109,13 +110,16 @@ SELECT * if (!empty($formats)) { $format_strings = array(); + $formats_exts = array(); foreach ($formats as $format) { $format_strings[] = sprintf('%s (%.2fMB)', $format['ext'], $format['filesize']/1024); + $formats_exts[] = strtolower($format['ext']); } $formats_original_info['formats'] = l10n('Formats: %s', implode(', ', $format_strings)); + $formats_ext_info = json_encode($formats_exts); } $extTab = explode('.',$formats_original_info['file']); @@ -150,7 +154,8 @@ $template->assign(array( 'DISPLAY_FORMATS' => $display_formats, 'HAVE_FORMATS_ORIGINAL' => $have_formats_original, 'FORMATS_ORIGINAL_INFO' => $formats_original_info, - 'SWITCH_MODE_URL' => get_root_url().'admin.php?page=photos_add'.($display_formats ? '':'&formats'), + 'FORMATS_EXT_INFO' => $formats_ext_info, + 'SWITCH_FORMAT_MODE_URL' => get_root_url().'admin.php?page=photos_add'.($display_formats ? '':'&formats'), 'format_ext' => implode(',', $conf['format_ext']), 'str_format_ext' => implode(', ', $conf['format_ext']), )); diff --git a/admin/themes/clear/theme.css b/admin/themes/clear/theme.css index cc5a09900..4fd1c5ef5 100644 --- a/admin/themes/clear/theme.css +++ b/admin/themes/clear/theme.css @@ -665,6 +665,9 @@ a#showPermissions:hover {border-color: #A5A5A5;} .plupload_filelist_footer {background-color: #F5F5F5!important;} li.plupload_delete a {background: url("images/cancel.svg")!important; background-size: cover!important;} li.plupload_delete a:hover {background: url("images/cancelhover.svg")!important; background-size: cover!important;} +li.plupload_delete a span {background: #FFF;} +li.plupload_delete a:hover span {background: #FFF;} +li.plupload_delete a.remove-format:hover{color: #474747;} .addAlbumEmpty {color: #3C3C3C;} #permitAction p {background: #FFF;} @@ -714,6 +717,13 @@ li.plupload_delete a:hover {background: url("images/cancelhover.svg")!important; #batchManagerGlobal .ui-slider-range.ui-widget-header.ui-corner-all {border: 1px solid #ffaf58;} #batchManagerGlobal .font-checkbox.selected {color: #777;} +/* Picture add */ + +.upload-options, .upload-options-content { + background-color: #f5f5f5; + color: #777777; +} + /* Category List */ .categoryContainer { margin: 0; diff --git a/admin/themes/default/js/photos_add_direct.js b/admin/themes/default/js/photos_add_direct.js index 975052ff8..4d361f5d7 100644 --- a/admin/themes/default/js/photos_add_direct.js +++ b/admin/themes/default/js/photos_add_direct.js @@ -16,6 +16,8 @@ const selectedAlbumEdit = $('#selectedAlbumEdit'); const btnAddFiles = $('#addFiles'); const chooseAlbumFirst = $('#chooseAlbumFirst'); const uploaderPhotos = $('#uploader'); +const formatsUpdated = []; +const formats = []; /*-------------- On DOM load @@ -85,6 +87,12 @@ $(function () { return false; }); + $("#uploadOptionsContent").hide(); + $("#uploadOptions").on("click", function(){ + $("#uploadOptionsContent").slideToggle(); + $(".moxie-shim-html5").css("display", "none"); + }) + $("#uploader").pluploadQueue({ // General settings browse_button: 'addFiles', @@ -153,11 +161,37 @@ $(function () { FilesAdded: async function (up, files) { // Création de la liste avec plupload_id : image_name fileNames = {}; + exts = {}; files.forEach((file) => { fileNames[file.id] = file.name; + exts[file.id] = file.name.substr(file.name.lastIndexOf('.') + 1); }); if (formatMode) { + formats.forEach((forms) => { + $("#"+forms[0]+" > .plupload_file_name").append(` + + + + `); + if(formatsUpdated.includes(forms[0])){ + $("#"+forms[0]+" > .plupload_file_name").after(` + + + ${format_update_warning} + + + + + + ${format_remove} + `); + $("#remove_"+forms[0]).on("click", function(){ + up.removeFile(forms[0]); + }); + } + }); + // If no original image is specified if (!haveFormatsOriginal) { const images_search = await new Promise((res, rej) => { @@ -166,8 +200,6 @@ $(function () { url: "ws.php?format=json&method=pwg.images.formats.searchImage", type: "POST", data: { - // category_id: $("select[name=category] option:selected").val(), // id category to modify - category_id: ab.get_selected_albums()[0], filename_list: JSON.stringify(fileNames), }, success: function (result) { @@ -182,8 +214,33 @@ $(function () { files.forEach((f) => { const search = images_search[f.id]; - if (search.status == "found") + if (search.status == "found"){ f.format_of = search.image_id; + formats.push([f.id,f.format_of]); + $("#"+f.id+" > .plupload_file_name").append(` + + + + `); + if (search.format_exist) + { + $("#"+f.id+" > .plupload_file_name").after(` + + + ${format_update_warning} + + + + + + ${format_remove} + `); + formatsUpdated.push(f.id); + $("#remove_"+f.id).on("click", function(){ + up.removeFile(f.id); + }); + } + } else { if (search.status == "multiple") multiple.push(f.name); @@ -218,14 +275,72 @@ $(function () { ...jConfirm_warning_options }) } - } else { //If an original image is specified + } else { + if (imageFormatsExtensions) + { + $forms_exts = JSON.parse(imageFormatsExtensions); + } + else + { + $forms_exts = []; + } files.forEach((f) => { f.format_of = originalImageId; + formats.push([f.id,f.format_of]); + $("#"+f.id+" > .plupload_file_name").append(` + + + + `); + if ($forms_exts.indexOf(exts[f.id]) != -1) + { + $("#"+f.id+" > .plupload_file_name").after(` + + + ${format_update_warning} + + + + + + ${format_remove} + `); + formatsUpdated.push(f.id); + $("#remove_"+f.id).on("click", function(){ + up.removeFile(f.id); + }); + } }) } } }, + FilesRemoved: function(up, file){ + formats.forEach((forms) => { + $("#"+forms[0]+" > .plupload_file_name").append(` + + + + `); + if(formatsUpdated.includes(forms[0])){ + $("#"+forms[0]+" > .plupload_file_name").after(` + + + ${format_update_warning} + + + + + + ${format_remove} + `); + $("#remove_"+forms[0]).on("click", function(){ + up.removeFile(forms[0]); + }); + } + }); + }, + UploadProgress: function (up, file) { $('#uploadingActions .progressbar').width(up.total.percent + '%'); Piecon.setProgress(up.total.percent); @@ -265,6 +380,8 @@ $(function () { options.name = file.name; } + options.update_mode = $('#toggleUpdateMode').is(':checked'); + up.setOption('multipart_params', options); }, @@ -289,6 +406,12 @@ $(function () { // do not remove file, or it will reset the progress bar :-/ // up.removeFile(file); uploadedPhotos.push(parseInt(data.result.image_id)); + if(data.result.add_status=="add"){ + addedPhotos.push(parseInt(data.result.image_id)); + } + else{ + updatedPhotos.push(parseInt(data.result.image_id)); + } if (!formatMode) uploadCategory = data.result.category; }, @@ -322,12 +445,23 @@ $(function () { $("#uploadForm, #permissions, .showFieldset").hide(); - const infoText = formatMode ? - sprintf(formatsUploaded_label, uploadedPhotos.length, [...new Set(files.map(f => f.format_of))].length) - : sprintf(photosUploaded_label, uploadedPhotos.length) + const infoTextAdd = formatMode ? + sprintf(formatsAdded_label, addedPhotos.length, [...new Set(addedPhotos)].length) + : sprintf(photosAdded_label, addedPhotos.length); - $(".infos").append('
  • ' + infoText + '
'); + const infoTextUpdate = formatMode ? + sprintf(formatsUpdated_label, updatedPhotos.length, [...new Set(updatedPhotos)].length) + : sprintf(photosUpdated_label, updatedPhotos.length); + if (addedPhotos.length && updatedPhotos.length) + { + $(".infos").append( '
  • ' + infoTextAdd + ', ' + infoTextUpdate + '
'); + } + else + { + const infoText = addedPhotos.length ? infoTextAdd : infoTextUpdate; + $(".infos").append('
  • ' + infoText + '
'); + } if (!formatMode) { html = sprintf( @@ -357,7 +491,6 @@ $(function () { } } }); - }); /*-------------- diff --git a/admin/themes/default/template/photos_add_direct.tpl b/admin/themes/default/template/photos_add_direct.tpl index 5353923c8..5c0ede7d4 100644 --- a/admin/themes/default/template/photos_add_direct.tpl +++ b/admin/themes/default/template/photos_add_direct.tpl @@ -35,25 +35,32 @@ const formatMode = {if $DISPLAY_FORMATS}true{else}false{/if}; const haveFormatsOriginal = {if $HAVE_FORMATS_ORIGINAL}true{else}false{/if}; const originalImageId = haveFormatsOriginal? '{if isset($FORMATS_ORIGINAL_INFO['id'])} {$FORMATS_ORIGINAL_INFO['id']} {else} -1 {/if}' : -1; +const imageFormatsExtensions = '{$FORMATS_EXT_INFO}'; const nb_albums = {$NB_ALBUMS|escape:javascript}; const chunk_size = '{$chunk_size}kb'; const max_file_size = '{$max_file_size}mb'; +const format_update_warning = "{'This format already exists, it will be overwritten !'|translate}"; +const format_remove = "{'Remove'|translate}"; var pwg_token = '{$pwg_token}'; -var photosUploaded_label = "{'%d photos uploaded'|translate|escape:javascript}"; -var formatsUploaded_label = "{'%d formats uploaded for %d photos'|translate|escape:javascript}"; -var batch_Label = "{'Manage this set of %d photos'|translate|escape:javascript}"; -var albumSummary_label = "{'Album "%s" now contains %d photos'|translate|escape:javascript}"; -var str_format_warning = "{'Error when trying to detect formats'|translate|escape:javascript}"; -var str_ok = "{'Ok'|translate|escape:javascript}"; -var str_format_warning_multiple = "{'There is multiple image in the database with the following names : %s.'|translate|escape:javascript}"; -var str_format_warning_notFound = "{'No picture found with the following name : %s.'|translate|escape:javascript}"; -var str_and_X_others = "{'and %d more'|translate|escape:javascript}"; +const photosAdded_label = "{'%d photos uploaded'|translate|escape:javascript}"; +const photosUpdated_label = "{'%d photos updated'|translate|escape:javascript}"; +const formatsAdded_label = "{'%d formats added for %d photos'|translate|escape:javascript}"; +const formatsUpdated_label = "{'%d formats updated for %d photos'|translate|escape:javascript}"; +const batch_Label = "{'Manage this set of %d photos'|translate|escape:javascript}"; +const albumSummary_label = "{'Album "%s" now contains %d photos'|translate|escape:javascript}"; +const str_format_warning = "{'Error when trying to detect formats'|translate|escape:javascript}"; +const str_ok = "{'Ok'|translate|escape:javascript}"; +const str_format_warning_multiple = "{'There is multiple image in the database with the following names : %s.'|translate|escape:javascript}"; +const str_format_warning_notFound = "{'No picture found with the following name : %s.'|translate|escape:javascript}"; +const str_and_X_others = "{'and %d more'|translate|escape:javascript}"; const str_upload_in_progress = "{'Upload in progress'|translate|escape:javascript}"; const str_drop_album_ab = '{'Drop into album'|@translate|escape:javascript}'; -var file_ext = "{$file_exts}"; -var format_ext = "{$format_ext}"; -var uploadedPhotos = []; -var uploadCategory = null; +const file_ext = "{$file_exts}"; +const format_ext = "{$format_ext}"; +const uploadedPhotos = []; +let uploadCategory = null; +const addedPhotos = []; +const updatedPhotos = []; let related_categories_ids = {$selected_category|json_encode}; {/footer_script} @@ -103,7 +110,7 @@ let related_categories_ids = {$selected_category|json_encode}; {if $ENABLE_FORMATS and $can_upload}
-
+ +
+ + \ No newline at end of file diff --git a/admin/themes/default/theme.css b/admin/themes/default/theme.css index b04db974c..260bf8497 100644 --- a/admin/themes/default/theme.css +++ b/admin/themes/default/theme.css @@ -1920,6 +1920,7 @@ img.ui-datepicker-trigger { } .rotate-element{ + display: inline-block; transform: rotate(90deg); } @@ -6522,13 +6523,15 @@ table.qsearch_help_table td { .font-checkbox [class*=icon-check]:before { font-size:16px; line-height:16px; - margin-right:0; + margin-right:5px; margin-left:0; + font-size: 145%; } .font-checkbox .icon-check-empty { position:relative; left:-1px; } + .font-checkbox input[type=checkbox], .font-checkbox input[type=radio] { display:none; } @@ -8008,3 +8011,50 @@ color:#FF7B00; box-shadow: 0px 4px 4px 0px #00000040; font-size: 10px; } + +/* Filters options */ +.filters-grid{ + display: grid; + grid-template-columns: 200px 118px 25px; + margin-bottom: 0px !important; +} +.select-views{ + background-color: #f3f3f3; + font-size: 11px; + border: none; + height: 16px; + margin-top: 4.5px; + padding-left: 10px; +} +.select-views-arrow{ + display: flex; + position: absolute; + margin-left: 299.9px; + margin-top: 5px; + pointer-events: none; +} +.select-views-admin{ + margin-left: 3px; +} + +.last-filters{ + display: block; + margin-bottom: 15px; + font-weight: bold; +} + +.filter-manager-options-container{ + display: inline-block; + font-size: 11px; + font-weight: normal !important; + border: 1px solid #777; + border-radius: 20px; + padding: 4px 10px; + cursor: pointer; + margin-right: 7.5px; + margin-bottom: 7.5px; +} + +.mcs-icon-options::before{ + margin-right: 5px; +} \ No newline at end of file diff --git a/admin/themes/roma/theme.css b/admin/themes/roma/theme.css index b01548ad7..000eac0de 100644 --- a/admin/themes/roma/theme.css +++ b/admin/themes/roma/theme.css @@ -224,7 +224,7 @@ body .ui-resizable-autohide .ui-resizable-handle { display: none; } /* use 'body #ui-datepicker-div .ui-datepicker-buttonpane .ui-state-error:hover { background: #7e3030;} INPUT[type="text"].large { width: 317px; } -.buttonLike:disabled, input[type="button"]:disabled, input[type="submit"]:disabled, input[type="reset"]:disabled { +.buttonLike:disabled, .filters-icon-check:disabled, input[type="button"]:disabled, input[type="submit"]:disabled, input[type="reset"]:disabled { color:#555; border-color:#666; cursor:not-allowed; @@ -971,8 +971,6 @@ table.qsearch_help_table td { border-color: #444; } -.font-checkbox [class*=icon-check]:before { color:#aaa; } - #cboxLoadedContent { background-color:#333 !important; } #watermarkPositionBox { @@ -993,6 +991,8 @@ table.qsearch_help_table td { .font-checkbox {color: #898888;} .font-checkbox:hover {color: #ededed;} +.font-checkbox:hover .icon-check-empty, .font-checkbox:hover .icon-check {color: #FF7700;} +.font-checkbox .icon-check {color: #FFA646;} .with-border legend, .with-border strong {color: #c0c0c0;} .font-checkbox.selected {color: #ffa646;} @@ -2394,4 +2394,34 @@ ul.jqtree-tree li.jqtree-ghost span.jqtree-line { .RenameTagPopInContainer { background-color:#444; +} + +/* Filters options */ +.select-views{ + background-color: #444444; + color: #898888; + border: 1px solid #666; +} + +.filter-manager-options-container{ + background-color: #333; + color: #aaa; + border-color: #333; +} + +.filter-manager-options-container:hover{ + background-color: #5D5D5D; +} + +.filter-manager-options-container.selected-filter-container{ + background-color: #777777; + color: #ddd; +} + +.filter-manager-options-container.selected-filter-container:hover{ + background-color: #aaa; +} + +label:has(> .filters-icon-check:disabled){ + color:#555; } \ No newline at end of file diff --git a/include/functions_search.inc.php b/include/functions_search.inc.php index 288d1f517..a66f4cde5 100644 --- a/include/functions_search.inc.php +++ b/include/functions_search.inc.php @@ -119,10 +119,34 @@ function get_regular_search_results($search, $images_where='') $image_ids_for_filter = array(); + if (isset($conf['filters_views'])) + { + $display_filters = unserialize($conf['filters_views']); + } + else + { + $display_filters = unserialize('a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}'); + } + + + foreach($display_filters as $filt_name => $filt_conf){ + if(isset($filt_conf['access'])) + { + if ($filt_conf['access'] == 'everybody' or ($filt_conf['access'] == 'admins-only' and is_admin()) or ($filt_conf['access'] == 'registered-users' and is_classic_user())) + { + $display_filters[$filt_name]['access'] = true; + } + else + { + $display_filters[$filt_name]['access'] = false; + } + } + } + // // allwords // - if (isset($search['fields']['allwords']) and !empty($search['fields']['allwords']['words']) and count($search['fields']['allwords']['fields']) > 0) + if (isset($search['fields']['allwords']) and !empty($search['fields']['allwords']['words']) and count($search['fields']['allwords']['fields']) > 0 and $display_filters['words']['access']) { $has_filters_filled = true; @@ -303,7 +327,7 @@ SELECT // // author // - if (isset($search['fields']['author']) and count($search['fields']['author']['words']) > 0) + if (isset($search['fields']['author']) and count($search['fields']['author']['words']) > 0 and $display_filters['author']['access']) { $has_filters_filled = true; @@ -327,7 +351,7 @@ SELECT // // filetypes // - if (!empty($search['fields']['filetypes'])) + if (!empty($search['fields']['filetypes']) and $display_filters['file_type']['access']) { $has_filters_filled = true; @@ -351,7 +375,7 @@ SELECT // // added_by // - if (!empty($search['fields']['added_by'])) + if (!empty($search['fields']['added_by']) and $display_filters['added_by']['access']) { $has_filters_filled = true; @@ -369,7 +393,7 @@ SELECT // // cat // - if (isset($search['fields']['cat']) and !empty($search['fields']['cat']['words'])) + if (isset($search['fields']['cat']) and !empty($search['fields']['cat']['words']) and $display_filters['album']['access']) { $has_filters_filled = true; @@ -403,7 +427,7 @@ SELECT // // date_posted // - if (!empty($search['fields']['date_posted']['preset'])) + if (!empty($search['fields']['date_posted']['preset']) and $display_filters['post_date']['access']) { $has_filters_filled = true; @@ -485,7 +509,7 @@ SELECT // // date_created // - if (!empty($search['fields']['date_created']['preset'])) + if (!empty($search['fields']['date_created']['preset']) and $display_filters['creation_date']['access']) { $has_filters_filled = true; @@ -567,7 +591,7 @@ SELECT // // ratios // - if (!empty($search['fields']['ratios'])) + if (!empty($search['fields']['ratios']) and $display_filters['ratio']['access']) { $has_filters_filled = true; @@ -598,7 +622,7 @@ SELECT // // ratings // - if ($conf['rate'] and !empty($search['fields']['ratings'])) + if ($conf['rate'] and !empty($search['fields']['ratings']) and $display_filters['rating']['access']) { $has_filters_filled = true; @@ -629,7 +653,7 @@ SELECT // // filesize // - if (!empty($search['fields']['filesize_min']) and !empty($search['fields']['filesize_max'])) + if (!empty($search['fields']['filesize_min']) and !empty($search['fields']['filesize_max']) and $display_filters['file_size']['access']) { $has_filters_filled = true; @@ -649,7 +673,7 @@ SELECT // // height // - if (!empty($search['fields']['height_min']) and !empty($search['fields']['height_max'])) + if (!empty($search['fields']['height_min']) and !empty($search['fields']['height_max']) and $display_filters['height']['access']) { $has_filters_filled = true; @@ -667,7 +691,7 @@ SELECT // // width // - if (!empty($search['fields']['width_min']) and !empty($search['fields']['width_max'])) + if (!empty($search['fields']['width_min']) and !empty($search['fields']['width_max']) and $display_filters['width']['access']) { $has_filters_filled = true; @@ -685,7 +709,7 @@ SELECT // // tags // - if (isset($search['fields']['tags']) and !empty($search['fields']['tags']['words'])) + if (isset($search['fields']['tags']) and !empty($search['fields']['tags']['words']) and $display_filters['tags']['access']) { $has_filters_filled = true; diff --git a/include/search_filters.inc.php b/include/search_filters.inc.php index 82e6591c2..adf8c3b65 100644 --- a/include/search_filters.inc.php +++ b/include/search_filters.inc.php @@ -6,11 +6,38 @@ // | file that was distributed with this source code. | // +-----------------------------------------------------------------------+ +if (isset($conf['filters_views'])) +{ + $filters_views = unserialize($conf['filters_views']); +} +else +{ + $filters_views = unserialize('a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}'); +} + +$template->assign('display_filter', $filters_views); + // we add isset($page['search_details']) in this condition because it only // applies to regular search, not the legacy qsearch. As Piwigo 14 will still // be able to show an old quicksearch result, we must check this condtion too. if ('search' == $page['section'] and isset($page['search_details'])) { + $display_filters = $filters_views; + + foreach($filters_views as $filt_name => $filt_conf){ + if(isset($filt_conf['access'])) + { + if ($filt_conf['access'] == 'everybody' or ($filt_conf['access'] == 'admins-only' and is_admin()) or ($filt_conf['access'] == 'registered-users' and is_classic_user())) + { + $display_filters[$filt_name]['access'] = true; + } + else + { + $display_filters[$filt_name]['access'] = false; + } + } + } + include_once(PHPWG_ROOT_PATH.'include/functions_search.inc.php'); $my_search = get_search_array($page['search']); @@ -40,7 +67,12 @@ if ('search' == $page['section'] and isset($page['search_details'])) $search_items_clause = '1=1'; } - if (isset($my_search['fields']['tags'])) + if (isset($my_search['fields']['allwords']) and !($display_filters['words']['access'])) + { + unset($my_search['fields']['allwords']); + } + + if (isset($my_search['fields']['tags']) and $display_filters['tags']['access']) { $filter_tags = array(); @@ -77,7 +109,12 @@ if ('search' == $page['section'] and isset($page['search_details'])) $my_search['fields']['tags']['words'] = array_intersect($my_search['fields']['tags']['words'], $filter_tag_ids); } - if (isset($my_search['fields']['author'])) + else if (isset($my_search['fields']['tags']) and !($display_filters['tags']['access'])) + { + unset($my_search['fields']['tags']); + } + + if (isset($my_search['fields']['author']) and $display_filters['author']['access']) { $filter_clause = get_clause_for_filter('author'); @@ -118,7 +155,12 @@ SELECT $my_search['fields']['author']['words'] = array_intersect($my_search['fields']['author']['words'], $author_names); } - if (isset($my_search['fields']['date_posted'])) + else if (isset($my_search['fields']['author']) and !($display_filters['author']['access'])) + { + unset($my_search['fields']['author']); + } + + if (isset($my_search['fields']['date_posted']) and $display_filters['post_date']['access']) { $filter_clause = get_clause_for_filter('date_posted'); $cache_key = $persistent_cache->make_key('filter_date_posted'.$user['id'].$user['cache_update_time']); @@ -220,7 +262,12 @@ SELECT $template->assign('DATE_POSTED', $counters); } - if (isset($my_search['fields']['date_created'])) + else if (isset($my_search['fields']['date_posted']) and !($display_filters['post_date']['access'])) + { + unset($my_search['fields']['date_posted']); + } + + if (isset($my_search['fields']['date_created']) and $display_filters['creation_date']['access']) { $filter_clause = get_clause_for_filter('date_created'); $cache_key = $persistent_cache->make_key('filter_date_created'.$user['id'].$user['cache_update_time']); @@ -324,8 +371,12 @@ SELECT $template->assign('DATE_CREATED', $counters); } + else if (isset($my_search['fields']['date_created']) and !($display_filters['creation_date']['access'])) + { + unset($my_search['fields']['date_created']); + } - if (isset($my_search['fields']['added_by'])) + if (isset($my_search['fields']['added_by']) and $display_filters['added_by']['access']) { $filter_clause = get_clause_for_filter('added_by'); @@ -388,11 +439,18 @@ SELECT $my_search['fields']['added_by'] = array_intersect($my_search['fields']['added_by'], $user_ids); } - if (isset($my_search['fields']['cat']) and !empty($my_search['fields']['cat']['words'])) + else if (isset($my_search['fields']['added_by']) and !($display_filters['added_by']['access'])) { - $fullname_of = array(); + unset($my_search['fields']['added_by']); + } - $query = ' + if (isset($my_search['fields']['cat']) and $display_filters['album']['access']) + { + if (!empty($my_search['fields']['cat']['words'])) + { + $fullname_of = array(); + + $query = ' SELECT id, uppercats @@ -400,26 +458,32 @@ SELECT INNER JOIN '.USER_CACHE_CATEGORIES_TABLE.' ON id = cat_id AND user_id = '.$user['id'].' WHERE id IN ('.implode(',', $my_search['fields']['cat']['words']).') ;'; - $result = pwg_query($query); + $result = pwg_query($query); - while ($row = pwg_db_fetch_assoc($result)) - { - $cat_display_name = get_cat_display_name_cache( - $row['uppercats'], - 'admin.php?page=album-' // TODO not sure it's relevant to link to admin pages - ); - $row['fullname'] = strip_tags($cat_display_name); + while ($row = pwg_db_fetch_assoc($result)) + { + $cat_display_name = get_cat_display_name_cache( + $row['uppercats'], + 'admin.php?page=album-' // TODO not sure it's relevant to link to admin pages + ); + $row['fullname'] = strip_tags($cat_display_name); - $fullname_of[$row['id']] = $row['fullname']; + $fullname_of[$row['id']] = $row['fullname']; + } + + $template->assign('fullname_of', json_encode($fullname_of)); + + // in case the search has forbidden albums for current user, we need to filter the search rule + $my_search['fields']['cat']['words'] = array_intersect($my_search['fields']['cat']['words'], array_keys($fullname_of)); } - - $template->assign('fullname_of', json_encode($fullname_of)); - - // in case the search has forbidden albums for current user, we need to filter the search rule - $my_search['fields']['cat']['words'] = array_intersect($my_search['fields']['cat']['words'], array_keys($fullname_of)); } - if (isset($my_search['fields']['filetypes'])) + else if (isset($my_search['fields']['cat']) and !($display_filters['album']['access'])) + { + unset($my_search['fields']['cat']); + } + + if (isset($my_search['fields']['filetypes']) and $display_filters['file_type']['access']) { $filter_clause = get_clause_for_filter('filetypes'); @@ -469,12 +533,17 @@ SELECT } } + else if (isset($my_search['fields']['filetypes']) and !($display_filters['file_type']['access'])) + { + unset($my_search['fields']['filetypes']); + } + // For rating if ($conf['rate']) { $template->assign('SHOW_FILTER_RATINGS', true); - if (isset($my_search['fields']['ratings'])) + if (isset($my_search['fields']['ratings']) and $display_filters['rating']['access']) { $filter_clause = get_clause_for_filter('ratings'); @@ -529,14 +598,22 @@ SELECT } $template->assign('RATING', $ratings); } + else if (isset($my_search['fields']['ratings']) and !($display_filters['rating']['access'])) + { + unset($my_search['fields']['ratings']); + } } else { $template->assign('SHOW_FILTER_RATINGS', false); + if (isset($my_search['fields']['ratings'])) + { + unset($my_search['fields']['ratings']); + } } // For filesize - if (isset($my_search['fields']['filesize_min']) && isset($my_search['fields']['filesize_max'])) + if (isset($my_search['fields']['filesize_min']) && isset($my_search['fields']['filesize_max']) and $display_filters['file_size']['access']) { $filter_clause = get_clause_for_filter('filesize'); @@ -582,8 +659,14 @@ SELECT $template->assign('FILESIZE', $filesize ); } + + else if (isset($my_search['fields']['filesize_min']) && isset($my_search['fields']['filesize_max']) and !($display_filters['file_size']['access'])) + { + unset($my_search['fields']['filesize_min']); + unset($my_search['fields']['filesize_max']); + } - if (isset($my_search['fields']['ratios'])) + if (isset($my_search['fields']['ratios']) and $display_filters['ratio']['access']) { $filter_clause = get_clause_for_filter('ratios'); @@ -648,11 +731,15 @@ SELECT $persistent_cache->set($cache_key, $ratios); } } - $template->assign('RATIOS', $ratios); } - if (isset($my_search['fields']['height_min']) and isset($my_search['fields']['height_max'])) + else if (isset($my_search['fields']['ratios']) and !($display_filters['ratio']['access'])) + { + unset($my_search['fields']['ratios']); + } + + if (isset($my_search['fields']['height_min']) and isset($my_search['fields']['height_max']) and $display_filters['height']['access']) { $filter_clause = get_clause_for_filter('height'); @@ -699,7 +786,13 @@ SELECT $template->assign('HEIGHT', $height); } - if (isset($my_search['fields']['width_min']) and isset($my_search['fields']['width_max'])) + else if (isset($my_search['fields']['height_min']) && isset($my_search['fields']['height_max']) and !($display_filters['height']['access'])) + { + unset($my_search['fields']['height_min']); + unset($my_search['fields']['height_max']); + } + + if (isset($my_search['fields']['width_min']) and isset($my_search['fields']['width_max']) and $display_filters['width']['access']) { $filter_clause = get_clause_for_filter('width'); @@ -746,6 +839,12 @@ SELECT $template->assign('WIDTH', $width); } + else if (isset($my_search['fields']['width_min']) && isset($my_search['fields']['width_max']) and !($display_filters['width']['access'])) + { + unset($my_search['fields']['width_min']); + unset($my_search['fields']['width_max']); + } + $template->assign( array( 'GP' => json_encode($my_search), diff --git a/install/config.sql b/install/config.sql index 1dc6915ea..d2e9ede61 100644 --- a/install/config.sql +++ b/install/config.sql @@ -80,3 +80,4 @@ INSERT INTO piwigo_config (param,value) VALUES ('index_search_in_set_action','tr INSERT INTO piwigo_config (param,value) VALUES ('upload_detect_duplicate','true'); INSERT INTO piwigo_config (param,value) VALUES ('webmaster_id','1'); INSERT INTO piwigo_config (param,value) VALUES ('use_standard_pages','true'); +INSERT INTO piwigo_config (param,value,comment) VALUES ('filters_views','a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}','Filters displays configuration'); diff --git a/install/db/178-database.php b/install/db/178-database.php new file mode 100644 index 000000000..9aa85dece --- /dev/null +++ b/install/db/178-database.php @@ -0,0 +1,24 @@ + diff --git a/search.php b/search.php index 0b81d86f7..c7391b3f7 100644 --- a/search.php +++ b/search.php @@ -28,9 +28,49 @@ $search = array( ); // list of filters in user preferences -// allwords, cat, tags, author, added_by, filetypes, date_posted -$default_fields = array('allwords', 'cat', 'tags', 'author'); -if (is_a_guest() or is_generic()) +// allwords, cat, tags, author, added_by, filetypes, date_posted, date_created, ratios, ratings (if rating is allowed in this Piwigo), height, width +//import the conf for the filters +if (isset($conf['filters_views'])) +{ + $filters_conf = unserialize($conf['filters_views']); +} +else +{ + $filters_conf = unserialize('a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}'); +} + +//change the name of the keys so that they can be used with this part of the program +$filters_conf = array_combine +( + array('allwords', + 'tags', + 'date_posted', + 'date_created', + 'cat', + 'author', + 'added_by', + 'filetypes', + 'ratios', + 'ratings', + 'filesize', + 'height', + 'width', + 'last_filters_conf' + ), + $filters_conf +); + +//get all default filters +$default_fields = array(); +foreach($filters_conf as $filt_name => $filt_conf){ + if(isset($filt_conf['default'])){ + if($filt_conf['default'] == true){ + $default_fields[] = $filt_name; + } + } +} + +if (is_a_guest() or is_generic() or $filters_conf['last_filters_conf']==false) { $fields = $default_fields; } @@ -131,7 +171,7 @@ SELECT } } -foreach (array('added_by', 'filetypes', 'date_posted') as $field) +foreach (array('added_by', 'filetypes', 'ratios', 'ratings') as $field) { if (in_array($field, $fields)) { @@ -139,6 +179,23 @@ foreach (array('added_by', 'filetypes', 'date_posted') as $field) } } +foreach (array('date_posted', 'date_created') as $field){ + if (in_array($field, $fields)) + { + $search['fields'][$field] = array( + 'preset' => '' + ); + } +} + +foreach (array('filesize_min', 'filesize_max', 'width_min', 'width_max', 'height_min', 'height_max') as $field) +{ + if (in_array($field, $fields)) + { + $search['fields'][$field] = ''; + } +} + list($search_uuid, $search_url) = save_search($search); redirect($search_url); ?> diff --git a/themes/default/js/mcs.js b/themes/default/js/mcs.js index ec20466ee..d1aa9f0cb 100644 --- a/themes/default/js/mcs.js +++ b/themes/default/js/mcs.js @@ -33,6 +33,8 @@ $(document).ready(function () { PS_params.search_id = search_id; empty_filters_list = []; + filters_to_remove = []; + // Setup word filter if (global_params.fields.allwords) { $(".filter-word").css("display", "flex"); @@ -76,6 +78,7 @@ $(document).ready(function () { empty_filters_list.push(PS_params.allwords); } + //Hide filter spinner $(".filter-spinner").hide(); @@ -87,7 +90,7 @@ $(document).ready(function () { items: global_params.fields.tags ? global_params.fields.tags.words : null, }); }); - + if (global_params.fields.tags) { $(".filter-tag").css("display", "flex"); $(".filter-manager-controller.tags").prop("checked", true); @@ -569,17 +572,13 @@ $(document).ready(function () { empty_filters_list.push(PS_params.ratings); } - else if (!show_filter_ratings) - { - updateFilters('ratings', 'add'); - } // Setup filesize filter if (global_params.fields.filesize_min != null && global_params.fields.filesize_max != null) { $(".filter-filesize").css("display", "flex"); $(".filter-manager-controller.filesize").prop("checked", true); - $(".filter.filter-filesize .slider-info").html(sprintf(sliders.filesizes.text,sliders.filesizes.selected.min,sliders.filesizes.selected.max,)); + $(".filter.filter-filesize .slider-info").html(sprintf(sliders.filesizes.text,sliders.filesizes.selected.min,sliders.filesizes.selected.max)); $('[data-slider=filesizes]').pwgDoubleSlider(sliders.filesizes); @@ -594,7 +593,7 @@ $(document).ready(function () { if( global_params.fields.filesize_min != null && global_params.fields.filesize_max > 0) { $(".filter-filesize").addClass("filter-filled"); - $(".filter.filter-filesize .search-words").html(sprintf(sliders.filesizes.text,sliders.filesizes.selected.min,sliders.filesizes.selected.max,)); + $(".filter.filter-filesize .search-words").html(sprintf(sliders.filesizes.text,sliders.filesizes.selected.min,sliders.filesizes.selected.max)); } else { @@ -622,13 +621,13 @@ $(document).ready(function () { if (global_params.fields.height_min != null && global_params.fields.height_max != null) { $(".filter-height").css("display", "flex"); $(".filter-manager-controller.height").prop("checked", true); - $(".filter.filter-height .slider-info").html(sprintf(sliders.heights.text,sliders.heights.selected.min,sliders.heights.selected.max,)); + $(".filter.filter-height .slider-info").html(sprintf(sliders.heights.text,sliders.heights.selected.min,sliders.heights.selected.max)); $('[data-slider=heights]').pwgDoubleSlider(sliders.heights); if( global_params.fields.height_min > 0 && global_params.fields.height_max > 0) { $(".filter-height").addClass("filter-filled"); - $(".filter.filter-height .search-words").html(sprintf(sliders.heights.text,sliders.heights.selected.min,sliders.heights.selected.max,)); + $(".filter.filter-height .search-words").html(sprintf(sliders.heights.text,sliders.heights.selected.min,sliders.heights.selected.max)); } else { @@ -656,13 +655,13 @@ $(document).ready(function () { if (global_params.fields.width_min != null && global_params.fields.width_max != null) { $(".filter-width").css("display", "flex"); $(".filter-manager-controller.width").prop("checked", true); - $(".filter.filter-width .slider-info").html(sprintf(sliders.widths.text,sliders.widths.selected.min,sliders.widths.selected.max,)); + $(".filter.filter-width .slider-info").html(sprintf(sliders.widths.text,sliders.widths.selected.min,sliders.widths.selected.max)); $('[data-slider=widths]').pwgDoubleSlider(sliders.widths); if( global_params.fields.width_min > 0 && global_params.fields.width_max > 0) { $(".filter-width").addClass("filter-filled"); - $(".filter.filter-width .search-words").html(sprintf(sliders.widths.text,sliders.widths.selected.min,sliders.widths.selected.max,)); + $(".filter.filter-width .search-words").html(sprintf(sliders.widths.text,sliders.widths.selected.min,sliders.widths.selected.max)); } else { @@ -686,6 +685,10 @@ $(document).ready(function () { empty_filters_list.push(PS_params.width_max); } + if(filters_to_remove.length > 0){ + filters_to_remove = []; + performSearch(PS_params, true); + } // Adapt no result message if ($(".filter-filled").length === 0) { @@ -693,7 +696,7 @@ $(document).ready(function () { $(".mcs-no-result .text .bot").html(str_empty_search_bot_alt); } - if (!empty_filters_list.every(param => param === "" || param === null)) { + if (!empty_filters_list.every(param => param === "" || param === null || (typeof param == 'undefined'))) { $(".clear-all").addClass("clickable"); $(".clear-all.clickable").on('click', function () { exclude_params = ['search_id', 'allwords_mode', 'allwords_fields', 'tags_mode', 'categories_withsubs']; diff --git a/themes/default/template/include/search_filters.inc.tpl b/themes/default/template/include/search_filters.inc.tpl index c1a5ff012..5eeb49575 100644 --- a/themes/default/template/include/search_filters.inc.tpl +++ b/themes/default/template/include/search_filters.inc.tpl @@ -23,6 +23,14 @@ fullname_of_cat = {$fullname_of}; search_id = '{$SEARCH_ID}'; {/if} +{if is_admin()} +user_rank = "admin"; +{elseif is_classic_user()} +user_rank = "user"; +{else} +user_rank = "none"; +{/if} + str_word_widget_label = "{'Search for words'|@translate|escape:javascript}"; str_tags_widget_label = "{'Tag'|@translate|escape:javascript}"; str_album_widget_label = "{'Album'|@translate|escape:javascript}"; @@ -101,60 +109,86 @@ const prefix_icon = 'gallery-icon-';
{'Choose filters'|@translate}
+ {if $display_filter.words.access == 'everybody' or ($display_filter.words.access == 'admins-only' and is_admin()) or ($display_filter.words.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.tags.access == 'everybody' or ($display_filter.tags.access == 'admins-only' and is_admin()) or ($display_filter.tags.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.post_date.access == 'everybody' or ($display_filter.post_date.access == 'admins-only' and is_admin()) or ($display_filter.post_date.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.creation_date.access == 'everybody' or ($display_filter.creation_date.access == 'admins-only' and is_admin()) or ($display_filter.creation_date.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.album.access == 'everybody' or ($display_filter.album.access == 'admins-only' and is_admin()) or ($display_filter.album.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.author.access == 'everybody' or ($display_filter.author.access == 'admins-only' and is_admin()) or ($display_filter.author.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.added_by.access == 'everybody' or ($display_filter.added_by.access == 'admins-only' and is_admin()) or ($display_filter.added_by.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.file_type.access == 'everybody' or ($display_filter.file_type.access == 'admins-only' and is_admin()) or ($display_filter.file_type.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.ratio.access == 'everybody' or ($display_filter.ratio.access == 'admins-only' and is_admin()) or ($display_filter.ratio.access == 'registered-users' and is_classic_user())} + {/if} {if $SHOW_FILTER_RATINGS and isset($SHOW_FILTER_RATINGS)} + {if $display_filter.rating.access == 'everybody' or ($display_filter.rating.access == 'admins-only' and is_admin()) or ($display_filter.rating.access == 'registered-users' and is_classic_user())} + {/if} {/if} + {if $display_filter.file_size.access == 'everybody' or ($display_filter.file_size.access == 'admins-only' and is_admin()) or ($display_filter.file_size.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.height.access == 'everybody' or ($display_filter.height.access == 'admins-only' and is_admin()) or ($display_filter.height.access == 'registered-users' and is_classic_user())} + {/if} + {if $display_filter.width.access == 'everybody' or ($display_filter.width.access == 'admins-only' and is_admin()) or ($display_filter.width.access == 'registered-users' and is_classic_user())} + {/if}
From 4dc2fc9f8d6a15c9690c64e2c160e42d56a3c6ce Mon Sep 17 00:00:00 2001 From: Perrom <107625315+Perrom@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:17:50 +0200 Subject: [PATCH 009/149] fixes #2386 update filters in users activity logs (#2399) * Add an action filter and a date filter. * Additional filters on a specific object (photo/album/group) are available from their dedicated administration page. * Performances of the page was improved : instead of loading 100k lines in activity table, we loop on 500 activity lines until 100 aggregated lines are found for the current page. --- admin/batch_manager_unit.php | 1 + admin/cat_modify.php | 1 + admin/group_list.php | 2 + admin/picture_modify.php | 1 + admin/themes/clear/theme.css | 17 + admin/themes/default/js/user_activity.js | 712 +++++++++++++++ .../default/template/batch_manager_unit.tpl | 7 +- admin/themes/default/template/cat_modify.tpl | 2 + admin/themes/default/template/group_list.tpl | 1 + .../default/template/picture_modify.tpl | 1 + .../themes/default/template/user_activity.tpl | 824 +++++------------- admin/themes/default/theme.css | 23 +- admin/themes/roma/theme.css | 36 +- admin/user_activity.php | 99 +++ include/config_default.inc.php | 2 +- include/ws_functions/pwg.php | 249 ++++-- 16 files changed, 1238 insertions(+), 740 deletions(-) create mode 100644 admin/themes/default/js/user_activity.js diff --git a/admin/batch_manager_unit.php b/admin/batch_manager_unit.php index e3cf6f331..642960c26 100644 --- a/admin/batch_manager_unit.php +++ b/admin/batch_manager_unit.php @@ -402,6 +402,7 @@ SELECT 'tag_selection' => $tag_selection, 'U_DOWNLOAD' => 'action.php?id='.$row['id'].'&part=e&pwg_token='.get_pwg_token().'&download', 'U_HISTORY' => get_root_url().'admin.php?page=history&filter_image_id='.$row['id'], + 'U_ACTIVITY' => get_root_url().'admin.php?page=user_activity&photo='.$row['id'], 'U_DELETE' => $admin_url_start.'&delete=1&pwg_token='.get_pwg_token(), 'U_SYNC' => $admin_url_start.'&sync_metadata=1', 'PATH'=>$row['path'], diff --git a/admin/cat_modify.php b/admin/cat_modify.php index 8fe25a4f4..af8f7cb44 100644 --- a/admin/cat_modify.php +++ b/admin/cat_modify.php @@ -199,6 +199,7 @@ $template->assign( 'U_ADD_PHOTOS_ALBUM' => $base_url.'photos_add&album='.$category['id'], 'U_CHILDREN' => $cat_list_url.'&parent_id='.$category['id'], 'U_MOVE' => $base_url.'albums&parent_id='.$category['id'], + 'U_ACTIVITY' => get_root_url().'admin.php?page=user_activity&album='.$category['id'], ) ); diff --git a/admin/group_list.php b/admin/group_list.php index 1cdcde2b2..fbdb8e832 100644 --- a/admin/group_list.php +++ b/admin/group_list.php @@ -102,6 +102,8 @@ SELECT u.'. $conf['user_fields']['username'].' AS username ) ); + $template->assign('U_ACTIVITY', get_root_url().'admin.php?page=user_activity&group='.$row['id']); + $group_counter++; } diff --git a/admin/picture_modify.php b/admin/picture_modify.php index 61b137e54..4485ff921 100644 --- a/admin/picture_modify.php +++ b/admin/picture_modify.php @@ -247,6 +247,7 @@ $template->assign( 'U_SYNC' => $admin_url_start.'&sync_metadata=1', 'U_DELETE' => $admin_url_start.'&delete=1&pwg_token='.get_pwg_token(), 'U_HISTORY' => get_root_url().'admin.php?page=history&filter_image_id='.$_GET['image_id'], + 'U_ACTIVITY' => get_root_url().'admin.php?page=user_activity&photo='.$_GET['image_id'], 'PATH'=>$row['path'], diff --git a/admin/themes/clear/theme.css b/admin/themes/clear/theme.css index ec311106f..ed5b8b726 100644 --- a/admin/themes/clear/theme.css +++ b/admin/themes/clear/theme.css @@ -442,6 +442,23 @@ label>p.group_select { color:#3c3c3c; } +/* Activity Tab in user manager */ + +.activity-date-selecter{ + background-color: #f9f9f9; + color : #3C3C3C; + border: 2px solid #D3D3D3; +} + +.activity-filter-container{ + color: #777777; + background-color: #FFFFFF; +} + +.selectize-control.user-selecter.single .selectize-input, .selectize-control.action-selecter.single .selectize-input{ + border-color: #D3D3D3 !important; +} + /* Selection mode */ .slider { diff --git a/admin/themes/default/js/user_activity.js b/admin/themes/default/js/user_activity.js new file mode 100644 index 000000000..8333177de --- /dev/null +++ b/admin/themes/default/js/user_activity.js @@ -0,0 +1,712 @@ +//{*<-- Getting and Displaying Activities -->*} + +if (additional_filt_type) +{ + object_filter = additional_filt_type; +} + +get_user_activity(activity_page, uid_filter, action_filter, object_filter, [date_min_filter, date_max_filter], additional_filt_value); + +function get_user_activity(page, uid, action, object, date, id) { + + $.ajax({ + url: "ws.php?format=json&method=pwg.activity.getList", + type: "POST", + dataType: "json", + data: { + page: page - 1, + uid: uid, + action : action, + object : object, + offset : page_offsets[page - 1], + date_min : date[0], + date_max : date[1], + id : additional_filt_value + }, + beforeSend: () => { + $('.tab').contents(':not(#-1):not(.loading)').remove(); + $(".loading").show(); + $('.pagination-arrow.rigth').addClass('unavailable'); + $('.pagination-arrow.left').addClass('unavailable'); + $(".pagination-item-container").hide(); + $(".user-update-spinner").addClass("icon-spin6"); + }, + success: (data) => { + /* console log to help debug + {* console.log(data); *}*/ + uid_filter = uid; + action_filter = action; + object_filter = object; + date_min_filter = date[0]; + date_max_filter = date[1]; + + //setCreationDate(data.result['result_lines'][data.result['result_lines'].length-1].date, data.result['result_lines'][0].date); + $(".loading").hide(); + + if (data.result['result_lines'].length > 0) + { + data.result['result_lines'].forEach(line => { + lineConstructor(line); + }); + } + else + { + emptyLine(); + } + + current_page_offset = page_offsets[page - 1]; + end_page = data.result['end_page']; + if (!(page_offsets.includes(data.result['page_offset']))) + { + page_offsets.push(data.result['page_offset']); + } + + $(".user-update-spinner").removeClass("icon-spin6"); + $(".pagination-item-container").show(); + update_pagination_menu(); + }, + error: (e) => { + console.log("ajax call failed"); + console.log(e); + } + }) +} + +function lineConstructor(line) { + let newLine = $("#-1").clone(); + + $(".tab-title").show(); + $(".activity-noresult").hide(); + newLine.removeClass("hide"); + + /* console log to help debug + {* console.log(line); *}*/ + newLine.attr("id", line.id); + + var final_albumInfos; + + //{* Determines wich string need to be placed in the line constructed *} + + if (line.counter > 1) { + // pluriel + switch (line.action) { + case "edit": + newLine.find(".action-type").addClass("icon-blue"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-pencil"); + + newLine.find(".action-name").html(actionType_edit); + switch (line.object) { + case "user": + final_albumInfos = actionInfos_users_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-user-1"); + + break; + case "album": + final_albumInfos = actionInfos_albums_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_groups_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photos_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tags_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + break; + } + + break; + + case "add": + newLine.find(".action-type").addClass("icon-green"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-plus"); + + newLine.find(".action-name").html(actionType_add); + switch (line.object) { + case "user": + final_albumInfos = actionInfos_users_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-user-1"); + + break; + case "album": + final_albumInfos = actionInfos_albums_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_groups_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photos_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tags_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + break; + } + + break; + + case "delete": + newLine.find(".action-type").addClass("icon-red"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-trash-1"); + + newLine.find(".action-name").html(actionType_delete); + switch (line.object) { + case "user": + final_albumInfos = actionInfos_users_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-user-1"); + + break; + case "album": + final_albumInfos = actionInfos_albums_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_groups_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photos_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tags_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + break; + } + + break; + + case "move": + newLine.find(".action-type").addClass("icon-yellow"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-move"); + + newLine.find(".action-name").html(actionType_move); + switch (line.object) { + case "album": + final_albumInfos = actionInfos_albums_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_groups_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photos_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tags_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + break; + } + + break; + + case "login": + newLine.find(".action-type").addClass("icon-purple"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-key"); + newLine.find(".action-section").addClass("icon-user-1"); + + newLine.find(".action-name").html(actionType_login); + + final_albumInfos = actionInfos_users_logged_in.replace('%d', line.counter); + + break; + + case "logout": + newLine.find(".action-type").addClass("icon-purple"); + if (line.user_id != 2) { + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + } else { + newLine.find(".user-pic").addClass(color_icons[line.object_id[0] % 5]); + } + newLine.find(".action-icon").addClass("icon-logout"); + newLine.find(".action-section").addClass("icon-user-1"); + + newLine.find(".action-name").html(actionType_logout); + + final_albumInfos = actionInfos_users_logged_out.replace('%d', line.counter); + + break; + + default: + newLine.find(".action-type").addClass("icon-purple"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + break; + } + } else { + // singulier + switch (line.action) { + case "edit": + newLine.find(".action-type").addClass("icon-blue"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-pencil"); + + newLine.find(".action-name").html(actionType_edit); + switch (line.object) { + case "user": + final_albumInfos = actionInfos_user_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-user-1"); + + break; + case "album": + final_albumInfos = actionInfos_album_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_group_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photo_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tag_edited.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + break; + } + + + break; + case "add": + newLine.find(".action-type").addClass("icon-green"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-plus"); + + newLine.find(".action-name").html(actionType_add); + switch (line.object) { + case "user": + final_albumInfos = actionInfos_user_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-user-1"); + + break; + case "album": + final_albumInfos = actionInfos_album_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_group_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photo_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tag_added.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + + break; + } + + break; + case "delete": + newLine.find(".action-type").addClass("icon-red"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-trash-1"); + + newLine.find(".action-name").html(actionType_delete); + switch (line.object) { + case "user": + final_albumInfos = actionInfos_user_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-user-1"); + + break; + case "album": + final_albumInfos = actionInfos_album_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_group_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photo_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tag_deleted.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + break; + } + + break; + case "move": + newLine.find(".action-type").addClass("icon-yellow"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-move"); + + newLine.find(".action-name").html(actionType_move); + switch (line.object) { + case "album": + final_albumInfos = actionInfos_album_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-folder-open"); + + break; + case "group": + final_albumInfos = actionInfos_group_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-users-1"); + + break; + case "photo": + final_albumInfos = actionInfos_photo_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-picture"); + + break; + case "tag": + final_albumInfos = actionInfos_tag_moved.replace('%d', line.counter); + newLine.find(".action-section").addClass("icon-tags"); + + break; + default: + final_albumInfos = line.counter + " " +line.object + " " + line.action; + break; + } + + break; + case "login": + newLine.find(".action-type").addClass("icon-purple"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + newLine.find(".action-icon").addClass("icon-key"); + newLine.find(".action-section").addClass("icon-user-1"); + + newLine.find(".action-name").html(actionType_login); + + final_albumInfos = actionInfos_user_logged_in.replace('%d', line.counter); + + break; + case "logout": + newLine.find(".action-type").addClass("icon-purple"); + if (line.user_id != 2) { + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + } else { + newLine.find(".user-pic").addClass(color_icons[line.object_id[0] % 5]); + } + newLine.find(".action-icon").addClass("icon-logout"); + newLine.find(".action-section").addClass("icon-user-1"); + + newLine.find(".action-name").html(actionType_logout); + + final_albumInfos = actionInfos_user_logged_out.replace('%d', line.counter); + + break; + + default: + newLine.find(".action-type").addClass("icon-purple"); + newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); + break; + } + } + + newLine.find(".action-infos-test").html(final_albumInfos); + + /* Action_section */ + newLine.find(".nb_items").html(line.counter); + + /* Date_section */ + newLine.find(".date-day").html(line.date); + newLine.find(".date-hour").html(line.hour); + + /* User _Section */ + newLine.find(".user-name").html(line.username); + newLine.find(".user-pic").html(get_initials(line.username)); + + /* Detail_section */ + newLine.find(".detail-item-1").html(line.ip_address); + newLine.find(".detail-item-1").attr("title", "IP"); + + if (line.detailsType == "script") { + newLine.find(".detail-item-2").html(line.details.script); + newLine.find(".detail-item-2").attr('title', 'Script'); + } else if (line.detailsType == "method") { + newLine.find(".detail-item-2").html(line.details.method); + newLine.find(".detail-item-2").attr('title', 'API Method'); + } + + if (line.details.agent) { + newLine.find(".detail-item-3").html(line.details.agent); + newLine.find(".detail-item-3").attr('title', line.details.agent); + } else if (line.details.users_string && line.action != "logout" && line.action != "login") { + newLine.find(".detail-item-3").html(line.details.users_string); + newLine.find(".detail-item-3").attr('title', users_key + ": " +line.details.users_string); + } else { + newLine.find(".detail-item-3").remove(); + } + + newLine.addClass("uid-" + line.user_id); + + displayLine(newLine); +} + +function displayLine(line) { + $(".tab").append(line); +} + +function emptyLine() { + $(".tab-title").hide(); + $(".activity-noresult").show(); +} + +function get_initials(username) { + let words = username.toUpperCase().split(" "); + let res = words[0][0]; + + if (words.length > 1 && words[1][0] !== undefined ) { + res += words[1][0]; + } + return res; +} + +function setCreationDate(startDate, endDate) { + $(".start-date").html(startDate) + + $(".end-date").html(endDate) +} + +//{* Pagination *} + +function move_to_page(page) { + if (page < 0) + return; + actual_page = page; + update_pagination_menu(page); + get_user_activity(page, uid_filter, action_filter, object_filter, [date_min_filter, date_max_filter], additional_filt_value); +} + +$('.pagination-arrow.rigth').on('click', () => { + move_to_page(actual_page + 1); +}) + +$('.pagination-arrow.left').on('click', () => { + move_to_page(actual_page - 1); +}) + +function update_pagination_menu(page) { + updateArrows(); + update_pagination_items(); + if (end_page && actual_page == 1) { + $('.pagination-container').hide(); + } else { + $('.pagination-container').show(); + } +} + +function updateArrows() { + if (actual_page == 1) { + $('.pagination-arrow.left').addClass('unavailable'); + } else { + $('.pagination-arrow.left').removeClass('unavailable'); + } + if (end_page) { + $('.pagination-arrow.rigth').addClass('unavailable'); + } else { + $('.pagination-arrow.rigth').removeClass('unavailable'); + } +} + +function update_pagination_items() { + $('.pagination-item-container a').remove(); + $('.pagination-item-container span').remove(); + + append_pagination_item(1); + + if (actual_page > 2) { + append_pagination_item(); + } + if (actual_page != 1) { + append_pagination_item(actual_page) + } + if (!end_page) { + append_pagination_item(); + } +} + +function append_pagination_item(page = null) { + if (page != null) { + let new_tag = $(page_item.replace(/%d/g, page)); + $('.pagination-item-container').append(new_tag); + if (actual_page == page) { + new_tag.addClass('actual'); + } + new_tag.on('click', () => { + move_to_page(new_tag.data('page')); + }) + } else { + $('.pagination-item-container').append($(page_ellipsis)); + } +} + +function page_reset(){ + activity_page = 1; + current_page_offset = 0; + page_offsets = [0]; + actual_page = 1; + end_page = false; +} + + +$(document).ready(function () { + $("h1").append(``+ (nb_users - 1) +``); + + $('select.user-selecter').on('change', function (user) { + if ($(".user-selecter .selectize-input").hasClass("full")) { + page_reset(); + if ($(".user-selecter .selectize-input .item").data("value") == 'none') + { + //{* call ajax sur activity list sans uid *} + get_user_activity(1, undefined, action_filter, object_filter, [date_min_filter, date_max_filter], additional_filt_value); + } + else + { + //{* call ajax sur activity list avec uid en param *} + get_user_activity(1, $(".user-selecter .selectize-input .item").data("value"), action_filter, object_filter, [date_min_filter, date_max_filter], additional_filt_value); + } + } + }); + + $('select.action-selecter').on('change', function (user) { + if ($(".action-selecter .selectize-input").hasClass("full")) { + page_reset(); + if ($(".action-selecter .selectize-input .item").data("value") == 'none') + { + //{* call ajax sur activity list sans action et object *} + if (additional_filt_type) + { + get_user_activity(1, uid_filter, undefined, object_filter, [date_min_filter, date_max_filter], additional_filt_value); + } + else + { + get_user_activity(1, uid_filter, undefined, undefined, [date_min_filter, date_max_filter], additional_filt_value); + } + } + else + { + //{* call ajax sur activity list avec action et object en param *} + object = $(".action-selecter .selectize-input .item").data("value").split("/")[0]; + action = $(".action-selecter .selectize-input .item").data("value").split("/")[1]; + get_user_activity(1, uid_filter, action, object, [date_min_filter, date_max_filter], additional_filt_value); + } + } + }); + + $('#date_min_activity').on('change', function(user) { + if ($('#date_min_activity').val()=='') + { + document.getElementById('date_max_activity').setAttribute("min", date_min); + } + else + { + document.getElementById('date_max_activity').setAttribute("min", $('#date_min_activity').val()); + } + get_user_activity(activity_page, uid_filter, action_filter, object_filter, [$('#date_min_activity').val(), date_max_filter], additional_filt_value); + }) + + $('#date_max_activity').on('change', function(user) { + if ($('#date_max_activity').val()=='') + { + document.getElementById('date_min_activity').setAttribute("max", date_max); + } + else + { + document.getElementById('date_min_activity').setAttribute("max", $('#date_max_activity').val()); + } + get_user_activity(activity_page, uid_filter, action_filter, object_filter, [date_min_filter, $('#date_max_activity').val()], additional_filt_value); + }) + + jQuery('.user-selecter').selectize(); + jQuery('.user-selecter')[0].selectize.setValue(null); + + jQuery('.action-selecter').selectize(); + jQuery('.action-selecter')[0].selectize.setValue(null); + + if (additional_filt_type) + { + $("#activityMoreFilters").addClass("extend-padding"); + } + else + { + $("#activityMoreFiltersContent").hide(); + } + //var used to prevent the user to interfere with the collapsible when it's toggling, to avoid some problems + var toggleTriggered = false; + $("#activityMoreFilters").on("click", function(){ + if ($("#activityMoreFiltersContent").css('display') == 'none' && toggleTriggered == false) + { + toggleTriggered = true; + $("#activityMoreFilters").addClass("extend-padding"); + $("#activityMoreFiltersContent").slideToggle(function(){ + toggleTriggered = false; + }); + } + else if ($("#activityMoreFiltersContent").css('display') == 'flex' && toggleTriggered == false) + { + toggleTriggered = true; + $("#activityMoreFiltersContent").slideToggle(function(){ + $("#activityMoreFilters").removeClass("extend-padding"); + toggleTriggered = false; + }); + } + }) +}); \ No newline at end of file diff --git a/admin/themes/default/template/batch_manager_unit.tpl b/admin/themes/default/template/batch_manager_unit.tpl index e10f95b2e..bb5497d15 100644 --- a/admin/themes/default/template/batch_manager_unit.tpl +++ b/admin/themes/default/template/batch_manager_unit.tpl @@ -150,9 +150,10 @@ pluginValues = []; imagename
- - - + + + + {if !url_is_remote($element.PATH)} diff --git a/admin/themes/default/template/cat_modify.tpl b/admin/themes/default/template/cat_modify.tpl index 7ce02d13f..0eecebf6f 100644 --- a/admin/themes/default/template/cat_modify.tpl +++ b/admin/themes/default/template/cat_modify.tpl @@ -45,6 +45,8 @@ const str_modal_ab = '{'New parent album'|@translate}';
+ + {if isset($U_MANAGE_ELEMENTS) } {/if} diff --git a/admin/themes/default/template/group_list.tpl b/admin/themes/default/template/group_list.tpl index a167122f9..39e159e2c 100644 --- a/admin/themes/default/template/group_list.tpl +++ b/admin/themes/default/template/group_list.tpl @@ -87,6 +87,7 @@ usersCache.selectize(jQuery('select.UserSearch')); + {'Activity'|@translate}
diff --git a/admin/themes/default/template/picture_modify.tpl b/admin/themes/default/template/picture_modify.tpl index bfcd55496..8c9c29a4f 100644 --- a/admin/themes/default/template/picture_modify.tpl +++ b/admin/themes/default/template/picture_modify.tpl @@ -102,6 +102,7 @@ const str_assoc_album_ab = '{'Associate to album'|translate|escape:javascript}'; + {if !url_is_remote($PATH)} diff --git a/admin/themes/default/template/user_activity.tpl b/admin/themes/default/template/user_activity.tpl index 5b352d088..4e5ba1f70 100644 --- a/admin/themes/default/template/user_activity.tpl +++ b/admin/themes/default/template/user_activity.tpl @@ -13,12 +13,26 @@ var usersCache = new UsersCache({ serverId: '{$CACHE_KEYS._hash}', rootUrl: '{$ROOT_URL}' }); +const nb_users = {$nb_users}; + +const additional_filt_type = '{$ADDITIONAL_FILT.type}'; +const additional_filt_value = {if $ADDITIONAL_FILT.type} {$ADDITIONAL_FILT.value} {else} null {/if}; const color_icons = ["icon-red", "icon-blue", "icon-yellow", "icon-purple", "icon-green"]; var activity_page = 1; +let current_page_offset = 0; +let page_offsets = [0]; let actual_page = 1; -let max_page = 1; +let end_page = false; let uid_filter; +let action_filter; +let object_filter; +let date_min_filter = '{$ACTIVITY_DATES.min}'; +let date_max_filter = '{$ACTIVITY_DATES.max}'; + +const date_min = '{$ACTIVITY_DATES.min}'; +const date_max = '{$ACTIVITY_DATES.max}'; + const page_ellipsis = '...' const page_item = '%d'; var create_selecter = true; @@ -98,624 +112,109 @@ var actionInfos_tags_deleted = "{'%d tags deleted'|translate}"; var actionInfos_tags_edited = "{'%d tags edited'|translate}"; var actionInfos_tags_moved = "{'%d tags moved'|translate}"; -{*<-- Getting and Displaying Activities -->*} - -get_user_activity(activity_page, uid_filter); - -function get_user_activity(page, uid) { - $.ajax({ - url: "ws.php?format=json&method=pwg.activity.getList", - type: "POST", - dataType: "json", - data: { - page: page - 1, - uid: uid, - }, - beforeSend: () => { - $('.tab').contents(':not(#-1):not(.loading)').remove(); - $(".loading").show(); - $('.pagination-arrow.rigth').addClass('unavailable'); - $('.pagination-arrow.left').addClass('unavailable'); - $(".pagination-item-container").hide(); - $(".user-update-spinner").addClass("icon-spin6"); - }, - success: (data) => { - /* console log to help debug */ - {* console.log(data); *} - uid_filter = uid; - - setCreationDate(data.result['result_lines'][data.result['result_lines'].length-1].date, data.result['result_lines'][0].date); - $(".loading").hide(); - - data.result['result_lines'].forEach(line => { - lineConstructor(line); - }); - - max_page = data.result['max_page']; - $(".user-update-spinner").removeClass("icon-spin6"); - $(".pagination-item-container").show(); - update_pagination_menu(); - }, - error: (e) => { - console.log("ajax call failed"); - console.log(e); - } - }) -} - -function lineConstructor(line) { - let newLine = $("#-1").clone(); - - newLine.removeClass("hide"); - - /* console log to help debug */ - {* console.log(line); *} - newLine.attr("id", line.id); - - var final_albumInfos; - - {* Determines wich string need to be placed in the line constructed *} - - if (line.counter > 1) { - // pluriel - switch (line.action) { - case "edit": - newLine.find(".action-type").addClass("icon-blue"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-pencil"); - - newLine.find(".action-name").html(actionType_edit); - switch (line.object) { - case "user": - final_albumInfos = actionInfos_users_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-user-1"); - - break; - case "album": - final_albumInfos = actionInfos_albums_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_groups_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photos_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tags_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - break; - } - - break; - - case "add": - newLine.find(".action-type").addClass("icon-green"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-plus"); - - newLine.find(".action-name").html(actionType_add); - switch (line.object) { - case "user": - final_albumInfos = actionInfos_users_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-user-1"); - - break; - case "album": - final_albumInfos = actionInfos_albums_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_groups_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photos_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tags_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - break; - } - - break; - - case "delete": - newLine.find(".action-type").addClass("icon-red"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-trash-1"); - - newLine.find(".action-name").html(actionType_delete); - switch (line.object) { - case "user": - final_albumInfos = actionInfos_users_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-user-1"); - - break; - case "album": - final_albumInfos = actionInfos_albums_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_groups_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photos_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tags_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - break; - } - - break; - - case "move": - newLine.find(".action-type").addClass("icon-yellow"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-move"); - - newLine.find(".action-name").html(actionType_move); - switch (line.object) { - case "album": - final_albumInfos = actionInfos_albums_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_groups_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photos_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tags_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - break; - } - - break; - - case "login": - newLine.find(".action-type").addClass("icon-purple"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-key"); - newLine.find(".action-section").addClass("icon-user-1"); - - newLine.find(".action-name").html(actionType_login); - - final_albumInfos = actionInfos_users_logged_in.replace('%d', line.counter); - - break; - - case "logout": - newLine.find(".action-type").addClass("icon-purple"); - if (line.user_id != 2) { - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - } else { - newLine.find(".user-pic").addClass(color_icons[line.object_id[0] % 5]); - } - newLine.find(".action-icon").addClass("icon-logout"); - newLine.find(".action-section").addClass("icon-user-1"); - - newLine.find(".action-name").html(actionType_logout); - - final_albumInfos = actionInfos_users_logged_out.replace('%d', line.counter); - - break; - - default: - newLine.find(".action-type").addClass("icon-purple"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - break; - } - } else { - // singulier - switch (line.action) { - case "edit": - newLine.find(".action-type").addClass("icon-blue"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-pencil"); - - newLine.find(".action-name").html(actionType_edit); - switch (line.object) { - case "user": - final_albumInfos = actionInfos_user_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-user-1"); - - break; - case "album": - final_albumInfos = actionInfos_album_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_group_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photo_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tag_edited.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - break; - } - - - break; - case "add": - newLine.find(".action-type").addClass("icon-green"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-plus"); - - newLine.find(".action-name").html(actionType_add); - switch (line.object) { - case "user": - final_albumInfos = actionInfos_user_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-user-1"); - - break; - case "album": - final_albumInfos = actionInfos_album_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_group_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photo_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tag_added.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - - break; - } - - break; - case "delete": - newLine.find(".action-type").addClass("icon-red"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-trash-1"); - - newLine.find(".action-name").html(actionType_delete); - switch (line.object) { - case "user": - final_albumInfos = actionInfos_user_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-user-1"); - - break; - case "album": - final_albumInfos = actionInfos_album_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_group_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photo_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tag_deleted.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - break; - } - - break; - case "move": - newLine.find(".action-type").addClass("icon-yellow"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-move"); - - newLine.find(".action-name").html(actionType_move); - switch (line.object) { - case "album": - final_albumInfos = actionInfos_album_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-folder-open"); - - break; - case "group": - final_albumInfos = actionInfos_group_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-users-1"); - - break; - case "photo": - final_albumInfos = actionInfos_photo_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-picture"); - - break; - case "tag": - final_albumInfos = actionInfos_tag_moved.replace('%d', line.counter); - newLine.find(".action-section").addClass("icon-tags"); - - break; - default: - final_albumInfos = line.counter + " " +line.object + " " + line.action; - break; - } - - break; - case "login": - newLine.find(".action-type").addClass("icon-purple"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - newLine.find(".action-icon").addClass("icon-key"); - newLine.find(".action-section").addClass("icon-user-1"); - - newLine.find(".action-name").html(actionType_login); - - final_albumInfos = actionInfos_user_logged_in.replace('%d', line.counter); - - break; - case "logout": - newLine.find(".action-type").addClass("icon-purple"); - if (line.user_id != 2) { - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - } else { - newLine.find(".user-pic").addClass(color_icons[line.object_id[0] % 5]); - } - newLine.find(".action-icon").addClass("icon-logout"); - newLine.find(".action-section").addClass("icon-user-1"); - - newLine.find(".action-name").html(actionType_logout); - - final_albumInfos = actionInfos_user_logged_out.replace('%d', line.counter); - - break; - - default: - newLine.find(".action-type").addClass("icon-purple"); - newLine.find(".user-pic").addClass(color_icons[line.user_id % 5]); - break; - } - } - - newLine.find(".action-infos-test").html(final_albumInfos); - - /* Action_section */ - newLine.find(".nb_items").html(line.counter); - - /* Date_section */ - newLine.find(".date-day").html(line.date); - newLine.find(".date-hour").html(line.hour); - - /* User _Section */ - newLine.find(".user-name").html(line.username); - newLine.find(".user-pic").html(get_initials(line.username)); - - /* Detail_section */ - newLine.find(".detail-item-1").html(line.ip_address); - newLine.find(".detail-item-1").attr("title", "IP"); - - if (line.detailsType == "script") { - newLine.find(".detail-item-2").html(line.details.script); - newLine.find(".detail-item-2").attr('title', 'Script'); - } else if (line.detailsType == "method") { - newLine.find(".detail-item-2").html(line.details.method); - newLine.find(".detail-item-2").attr('title', 'API Method'); - } - - if (line.details.agent) { - newLine.find(".detail-item-3").html(line.details.agent); - newLine.find(".detail-item-3").attr('title', line.details.agent); - } else if (line.details.users_string && line.action != "logout" && line.action != "login") { - newLine.find(".detail-item-3").html(line.details.users_string); - newLine.find(".detail-item-3").attr('title', users_key + ": " +line.details.users_string); - } else { - newLine.find(".detail-item-3").remove(); - } - - newLine.addClass("uid-" + line.user_id); - - displayLine(newLine); -} - -function displayLine(line) { - $(".tab").append(line); -} - -function get_initials(username) { - let words = username.toUpperCase().split(" "); - let res = words[0][0]; - - if (words.length > 1 && words[1][0] !== undefined ) { - res += words[1][0]; - } - return res; -} - -function setCreationDate(startDate, endDate) { - $(".start-date").html(startDate) - - $(".end-date").html(endDate) -} - -{* Pagination *} - -function move_to_page(page) { - if (page < 0 || page > max_page) - return; - actual_page = page; - update_pagination_menu(); - get_user_activity(page, uid_filter); -} - -$('.pagination-arrow.rigth').on('click', () => { - move_to_page(actual_page + 1); -}) - -$('.pagination-arrow.left').on('click', () => { - move_to_page(actual_page - 1); -}) - -function update_pagination_menu() { - {* max_page = Math.ceil(nb_filtered_users / per_page); *} - updateArrows(); - update_pagination_items(); - if (max_page <= 1) { - $('.pagination-container').hide(); - } else { - $('.pagination-container').show(); - } -} - -function updateArrows() { - if (actual_page == 1) { - $('.pagination-arrow.left').addClass('unavailable'); - } else { - $('.pagination-arrow.left').removeClass('unavailable'); - } - if (actual_page == max_page) { - $('.pagination-arrow.rigth').addClass('unavailable'); - } else { - $('.pagination-arrow.rigth').removeClass('unavailable'); - } -} - -function update_pagination_items() { - $('.pagination-item-container a').remove(); - $('.pagination-item-container span').remove(); - - append_pagination_item(1); - - if (actual_page > 2) { - append_pagination_item(); - } - if (actual_page != 1 && actual_page != max_page) { - append_pagination_item(actual_page) - } - if (actual_page < (max_page - 1)) { - append_pagination_item(); - } - append_pagination_item(max_page); - -} - -function append_pagination_item(page = null) { - if (page != null) { - let new_tag = $(page_item.replace(/%d/g, page)); - $('.pagination-item-container').append(new_tag); - if (actual_page == page) { - new_tag.addClass('actual'); - } - new_tag.on('click', () => { - move_to_page(new_tag.data('page')); - }) - } else { - $('.pagination-item-container').append($(page_ellipsis)); - } -} - - -$(document).ready(function () { - $("h1").append(``+{$nb_users - 1}+``); - - $('select').on('change', function (user) { - if ($(".selectize-input").hasClass("full")) { - {* call ajax sur activity list avec uid en param *} - get_user_activity(1, $(".selectize-input .item").data("value")); - } - }); - - jQuery('.user-selecter').selectize(); - jQuery(".user-selecter")[0].selectize.setValue(null); - - jQuery(".cancel-icon").click(function() { - jQuery(".user-selecter")[0].selectize.clear(true); - $(".line").css('display', 'flex'); - }); -}); - {/footer_script} +{combine_script id='user_activity' load='async' require='jquery' path='admin/themes/default/js/user_activity.js'}
+
+
+
+ + + +
+ {'Filters'|@translate} +
+
+
+
+
+ {'User'|translate} + + +
-
-
- {'Selected user'|translate} +
+ {'Action'|translate} - + +
- +
+ {'Start-Date'|translate} + +
+ +
+ {'End-Date'|translate} + +
+ + {if $ADDITIONAL_FILT.type} +
+
+ {'Additional filters'|translate} +
+
+
+ {if $ADDITIONAL_FILT.type == 'photo'} + {$ADDITIONAL_FILT.name} + {else if $ADDITIONAL_FILT.type == 'album'} + {$ADDITIONAL_FILT.name} + {else} + {$ADDITIONAL_FILT.name} + {/if} +
+
+
+ {/if}
-
- {'Activity time from'|translate} - - - - {'to'|translate} - - - -
- - -
-{if max_page != 1} +
@@ -727,7 +226,11 @@ $(document).ready(function () {
-{/if} + +
+ {'No results'|translate} +
+
@@ -937,30 +440,105 @@ $(document).ready(function () { display: flex; flex-direction: row; - align-items: center; - - height: 100px; + margin-top: 38px; width: 100%; } -.select-user span { +div:has(> .activity-header) { + margin-bottom: 38px; +} + +.activity-select span { font-size: 15px; font-weight: bold; - - margin-right: 20px; } -.acivity-time { - margin: 0 25px; +.user-selecter, .action-selecter { + width: 230px; + margin-top: 10px; } -.user-selecter { - width: 150px; +.actions-filters{ + margin-left: 25%; } +.user_activity_end_options{ + margin-left: auto; + display: flex; +} + +.activity-noresult{ + opacity: 0.3; + text-align: center; + font-weight: bold; + font-size: 32px; + display: none; +} + +.activity-more-filters{ + margin-left: 14px; + padding: 4px 16px 4px 16px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.activity-more-filters.extend-padding{ + padding-bottom: 10px; +} + +.activity-more-filters, .activity-more-filters-content{ + background-color: #F3F3F3; +} + +.activity-more-filters-content{ + display: flex; + position: relative; + flex-direction: row; + font-weight: normal; + padding : 23px 0px 22px 24px; + width: auto; +} + +.activity-period-info{ + margin-bottom : 30px; + font-weight: bold; +} + +.additional-filters-section{ + margin-left: 5%; +} + +.additional-filters-info{ + margin-bottom : 18px; + font-weight: bold; +} + +.additional-filters{ + display: flex; +} + +.activity-filter-container span::before{ + margin-right: 6px; +} + +.activity-filter-container .icon-cancel{ + margin-left: 5px; +} + +.activity-date-selecter{ + display: block; + height: 25.5px; + width: 130px; + margin-top: 10px; + font-size: 12px; + font-weight: bold; +} /* Selectize */ -.selectize-control.single.user-selecter { +.selectize-control.single.user-selecter, .selectize-control.single.action-selecter { height: 30px; } diff --git a/admin/themes/default/theme.css b/admin/themes/default/theme.css index 260bf8497..6041109d7 100644 --- a/admin/themes/default/theme.css +++ b/admin/themes/default/theme.css @@ -7253,17 +7253,16 @@ color:#FF7B00; /* Activity Tab in user manager */ -.select-user { - background: #fafafa; +.activity-select { height: 50%; + text-align: left; + margin-right: 20px; +} - display: flex; - align-items: center; - - padding: 0 20px; - - box-shadow: 0px 2px 4px #00000024; - border-radius: 5px; +.activity-filter-container{ + border-radius: 20px; + padding: 4px 10px; + margin-right: 6px; } .start-date, @@ -7275,11 +7274,6 @@ color:#FF7B00; white-space: nowrap; } -.acivity-time-text { - font-size: 13px; - font-weight: bold; -} - .line { background: #fafafa; box-shadow: 0px 2px 4px #00000024; @@ -7311,7 +7305,6 @@ color:#FF7B00; } .download_csv { - margin-left: auto; height: 25px; width: 30px; background: #dddddd; diff --git a/admin/themes/roma/theme.css b/admin/themes/roma/theme.css index 000eac0de..ea0521ae5 100644 --- a/admin/themes/roma/theme.css +++ b/admin/themes/roma/theme.css @@ -1915,23 +1915,28 @@ background:#6C2D2D!important; /* Activity Tab in user manager */ -.select-user { - background: #555555; - height: 50%; - - display: flex; - align-items: center; - - padding: 0 20px; - - box-shadow: 0px 2px 4px #00000024; - border-radius: 5px; +.activity-more-filters, .activity-more-filters-content { + background-color: #343434 !important; } -.select-user-title { +.selectize-control.user-selecter.single .selectize-input, .selectize-control.action-selecter.single .selectize-input { + border-color: #555555 !important; +} + +.activity-select-title { color: #bbbbbb; } +.activity-date-selecter { + background-color: #555555; + color: #D5D5D5; +} + +.activity-filter-container{ + background-color: #555555; + color: #D5D5D5; +} + .start-date, .end-date { padding: 5px 10px; @@ -1941,12 +1946,6 @@ background:#6C2D2D!important; color: #bbbbbb; } -.acivity-time-text { - font-size: 13px; - font-weight: bold; - color: #bbbbbb; -} - /* Strange why this is different from light mode */ .compactView .user-container-initials-wrapper > span { height: 40px !important; @@ -1988,7 +1987,6 @@ background:#6C2D2D!important; } .download_csv { - margin-left: auto; height: 25px; width: 30px; background: #555555; diff --git a/admin/user_activity.php b/admin/user_activity.php index dd31df050..f62d34014 100644 --- a/admin/user_activity.php +++ b/admin/user_activity.php @@ -142,6 +142,105 @@ SELECT COUNT(*) list($nb_users) = pwg_db_fetch_row(pwg_query($query)); $template->assign('nb_users', $nb_users); +$query = ' +SELECT + occured_on + FROM '.ACTIVITY_TABLE.' + WHERE object != \'system\' + ;'; + +$result = query2array($query); + +$dates = array(); + +foreach($result as $time){ + list($date, $hour) = explode(' ', $time['occured_on']); + $dates[] = date_format(date_create($date),"Y-m-d"); +} + +$dates = array_unique($dates); +$list_dates['allDates'] = implode(',',$dates); +$list_dates['min'] = $dates[0]; +$list_dates['max'] = end($dates); + +$template->assign('ACTIVITY_DATES', $list_dates); + +$additional_filt_type = false; +$additional_filt_name = null; +$additional_filt_value = null; + +if(isset($_GET['photo'])) +{ + $query = ' + SELECT + name + FROM '.IMAGES_TABLE.' + WHERE id = '.$_GET['photo'].';'; + + $additional_filt_type = 'photo'; + $additional_filt_name = query2array($query)[0]['name']; + $additional_filt_value = $_GET['photo']; +} +else if (isset($_GET['album'])) +{ + $query = ' + SELECT + name + FROM '.CATEGORIES_TABLE.' + WHERE id = '.$_GET['album'].';'; + + $additional_filt_type = 'album'; + $additional_filt_name = query2array($query)[0]['name']; + $additional_filt_value = $_GET['album']; +} +else if (isset($_GET['group'])) +{ + $query = ' + SELECT + name + FROM '.GROUPS_TABLE.' + WHERE id = '.$_GET['group'].';'; + + $additional_filt_type = 'group'; + $additional_filt_name = query2array($query)[0]['name']; + $additional_filt_value = $_GET['group']; +} + +$template->assign('ADDITIONAL_FILT', array( + 'type' => $additional_filt_type, + 'name' => $additional_filt_name, + 'value' => $additional_filt_value +)); + +$query = ' +SELECT + object, + action, + count(*) AS counter + FROM '.ACTIVITY_TABLE.' + WHERE object != \'system\''; + + if ($additional_filt_type) + { + $query .= ' + AND object = "'.$additional_filt_type.'"'; + } + + $query .= ' + GROUP BY + action, + object + ORDER BY object ASC + ;'; + + +$actions = query2array($query); +foreach($actions as &$action){ + $action['value'] = $action['object'].'/'.$action['action']; +} + +$template->assign('ACTIONS', $actions); + $template->assign_var_from_handle('ADMIN_CONTENT', 'user_activity'); ?> \ No newline at end of file diff --git a/include/config_default.inc.php b/include/config_default.inc.php index 21336b389..5a1cccd93 100644 --- a/include/config_default.inc.php +++ b/include/config_default.inc.php @@ -811,7 +811,7 @@ $conf['dashboard_activity_nb_weeks'] = 4; // 'all' = do not filter, display all // 'admins_only' = only display connections of admin users // 'none' = don't even display connections of admin users -$conf['activity_display_connections'] = 'admins_only'; +$conf['activity_display_connections'] = 'all'; // On album mover page, number of seconds before auto openning album when // dragging an album. In milliseconds. 3 seconds by default. diff --git a/include/ws_functions/pwg.php b/include/ws_functions/pwg.php index c1886b189..bbb041180 100644 --- a/include/ws_functions/pwg.php +++ b/include/ws_functions/pwg.php @@ -459,12 +459,77 @@ function ws_getActivityList($param, &$service) $output_lines = array(); $current_key = ''; - $page_size = 100000; //We will fetch X lines in database =/= lines displayed due to line concatenation - $page_offset = $param['page']*$page_size; + $page_size = 100; //We will fetch X lines in database =/= lines displayed due to line concatenation + //$page_offset = $param['page']*$page_size; + $page_offset = $param['offset']; $user_ids = array(); - $query = ' + $line_id = 0; + + + + if (!empty($param['date_min'])) { + $min = date_format(date_create($param['date_min']), "Y-m-d H:i:s"); + $max = date_format(date_create($param['date_max']), "Y-m-d 23:59:59"); + } + + if (!empty($param['date_max'])) { + $max = date_format(date_create($param['date_max']), "Y-m-d 23:59:59"); + } + + if (isset($param['uid'])) { + $query = ' + SELECT + count(*) + FROM '.ACTIVITY_TABLE.' + WHERE object != \'system\' + AND performed_by = '.$param['uid']; + } else { + $query = ' + SELECT + count(*) + FROM '.ACTIVITY_TABLE.' + WHERE object != \'system\''; + } + + if (isset($param['action'])) + { + $query .= ' + AND action = "'.$param['action'].'"'; + } + + if (isset($param['object'])) + { + $query .= ' + AND object = "'.$param['object'].'"'; + } + + if (!empty($param['date_min'])) + { + $query .= ' + AND occured_on >= "'.$min.'"'; + } + + if (!empty($param['date_max'])) + { + $query .= ' + AND occured_on <= "'.$max.'"'; + } + + if (!empty($param['id'])) + { + $query .= ' + AND object_id = '.$param['id']; + } + + $query .= ';'; + + $max_line = (pwg_db_fetch_row(pwg_query($query))[0]); + + while(sizeof($output_lines) < $page_size and !($page_offset >= $max_line)) + { + $query = ' SELECT activity_id, performed_by, @@ -479,11 +544,43 @@ SELECT FROM '.ACTIVITY_TABLE.' WHERE object != \'system\''; - if (isset($param['uid'])) + if (isset($param['uid'])) + { + $query.= ' + AND performed_by = '.$param['uid']; + } + + if (isset($param['action'])) + { + $query .= ' + AND action = "'.$param['action'].'"'; + } + + if (isset($param['object'])) + { + $query .= ' + AND object = "'.$param['object'].'"'; + } + + if (!empty($param['date_min'])) { - $query.= ' - AND performed_by = '.$param['uid']; + $query .= ' + AND occured_on >= "'.$min.'"'; } + + if (!empty($param['date_max'])) + { + $query .= ' + AND occured_on <= "'.$max.'"'; + } + + if (!empty($param['id'])) + { + $id = pwg_db_real_escape_string(stripslashes($param['id'])); + $query .= ' + AND object_id = '.$id; + } + elseif ('none' == $conf['activity_display_connections']) { $query.= ' @@ -496,67 +593,77 @@ SELECT AND NOT (action IN (\'login\', \'logout\') AND object_id NOT IN ('.implode(',', get_admins()).'))'; } - $query.= ' + $query.= ' ORDER BY activity_id DESC - LIMIT '.$page_size.' OFFSET '.$page_offset.' + LIMIT '.($page_size * 5).' OFFSET '.$page_offset.' ;'; - $line_id = 0; - $result = pwg_query($query); - while ($row = pwg_db_fetch_assoc($result)) - { - $row['details'] = str_replace('`groups`', 'groups', $row['details']); - $row['details'] = str_replace('`rank`', 'rank', $row['details']); - $details = @unserialize($row['details']); + $result = pwg_query($query); - if (isset($row['user_agent'])) + while ($row = pwg_db_fetch_assoc($result)) { - $details['agent'] = $row['user_agent']; - } - - if (isset($details['method'])) - { - $detailsType = 'method'; - } - if (isset($details['script'])) - { - $detailsType = 'script'; - } - - $line_key = $row['session_idx'].'~'.$row['object'].'~'.$row['action'].'~'; // idx~photo~add - - if ($line_key === $current_key) - { - // I increment the counter of the previous line - $output_lines[count($output_lines)-1]['counter']++; - $output_lines[count($output_lines)-1]['object_id'][] = $row['object_id']; - } - else - { - list($date, $hour) = explode(' ', $row['occured_on']); - // New line - $output_lines[] = array( - 'id' => $line_id, - 'object' => $row['object'], - 'object_id' => array($row['object_id']), - 'action' => $row['action'], - 'ip_address' => $row['ip_address'], - 'date' => format_date($date), - 'hour' => $hour, - 'user_id' => $row['performed_by'], - 'detailsType' => $detailsType, - 'details' => $details, - 'counter' => 1, - ); - - $user_ids[ $row['performed_by'] ] = 1; - if ('user' == $row['object']) + if (sizeof($output_lines) < $page_size) { - $user_ids[ $row['object_id'] ] = 1; - } + $page_offset++; - $current_key = $line_key; - $line_id++; + $row['details'] = str_replace('`groups`', 'groups', $row['details']); + $row['details'] = str_replace('`rank`', 'rank', $row['details']); + $details = @unserialize($row['details']); + + if (isset($row['user_agent'])) + { + $details['agent'] = $row['user_agent']; + } + + if (isset($details['method'])) + { + $detailsType = 'method'; + } + if (isset($details['script'])) + { + $detailsType = 'script'; + } + + $line_key = $row['session_idx'].'~'.$row['object'].'~'.$row['action'].'~'; // idx~photo~add + + if ($line_key === $current_key) + { + // I increment the counter of the previous line + $output_lines[count($output_lines)-1]['counter']++; + $output_lines[count($output_lines)-1]['object_id'][] = $row['object_id']; + } + else + { + list($date, $hour) = explode(' ', $row['occured_on']); + // New line + $output_lines[] = array( + 'id' => $line_id, + 'object' => $row['object'], + 'object_id' => array($row['object_id']), + 'action' => $row['action'], + 'ip_address' => $row['ip_address'], + 'date' => format_date($date), + 'hour' => $hour, + 'user_id' => $row['performed_by'], + 'detailsType' => $detailsType, + 'details' => $details, + 'counter' => 1, + ); + + $user_ids[ $row['performed_by'] ] = 1; + if ('user' == $row['object']) + { + $user_ids[ $row['object_id'] ] = 1; + } + + $current_key = $line_key; + $line_id++; + } + } + else + { + break; + } } } @@ -596,27 +703,11 @@ SELECT } } - if (isset($param['uid'])) { - $query = ' - SELECT - count(*) - FROM '.ACTIVITY_TABLE.' - WHERE performed_by = '.$param['uid'].' - ;'; - } else { - $query = ' - SELECT - count(*) - FROM '.ACTIVITY_TABLE.' - ;'; - } - - $result = (pwg_db_fetch_row(pwg_query($query))[0])/$page_size; - return array( 'result_lines' => $output_lines, - 'max_page' => floor($result), - 'params' => $param, + 'page_offset' => $page_offset, + 'end_page' => ($page_offset >= $max_line), + 'params' => $param ); } From 80ab463808aa2c8b13833c7266267f1bf1590733 Mon Sep 17 00:00:00 2001 From: Martin R <167065719+Martin-Raby@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:47:56 +0200 Subject: [PATCH 010/149] Issue #2364 redesign user comment manager (PR #2400) *Ability to reject or validate one by one *Filter by : status : validated, user, user status, begin date, end date *refreshed design to match current piwigo design *[TODO] search input does not work, *[TODO] user name duplicate in user filter (use user id instead of user name to filter) *[TODO] filter by image is missing *[TODO] confirmation when a comment is validated or deleted is missing --- admin/comments.php | 48 ++- admin/themes/default/template/comments.tpl | 365 +++++++++++++++++++-- admin/themes/default/theme.css | 288 +++++++++++++++- admin/themes/roma/theme.css | 58 ++++ 4 files changed, 723 insertions(+), 36 deletions(-) diff --git a/admin/comments.php b/admin/comments.php index 2cd6b313b..cb2f4a7d2 100644 --- a/admin/comments.php +++ b/admin/comments.php @@ -108,6 +108,7 @@ $tabsheet->assign(); $nb_total = 0; $nb_pending = 0; +$nb_validated = 0; $query = ' SELECT @@ -127,6 +128,8 @@ while ($row = pwg_db_fetch_assoc($result)) } } +$nb_validated = $nb_total - $nb_pending; + if (!isset($_GET['filter']) and $nb_pending > 0) { $page['filter'] = 'pending'; @@ -136,16 +139,50 @@ else $page['filter'] = 'all'; } -if (isset($_GET['filter']) and 'pending' == $_GET['filter']) +if (isset($_GET['filter']) and ('pending' == $_GET['filter'] or 'validated' == $_GET['filter'])) { $page['filter'] = $_GET['filter']; } +if (isset($_GET['status'])) +{ + $displayed_status = $_GET['status']; +} +else +{ + $displayed_status = 'all'; +} + +if (isset($_GET['author'])) +{ + $author = $_GET['author']; +} +else +{ + $author = 'all'; +} + +// by default, no filter by date is active +$start = $end = ""; + +if (isset($_GET['start_date'])){ + $start = $_GET['start_date']; +} + +if (isset($_GET['end_date'])){ + $end = $_GET['end_date']; +} + $template->assign( array( 'nb_total' => $nb_total, 'nb_pending' => $nb_pending, + 'nb_validated' => $nb_validated, 'filter' => $page['filter'], + 'displayed_status' => $displayed_status, + 'displayed_author' => $author, + 'START' => $start, + 'END' => $end, ) ); @@ -155,6 +192,10 @@ if ('pending' == $page['filter']) { $where_clauses[] = 'validated=\'false\''; } +if ('validated' == $page['filter']) +{ + $where_clauses[] = 'validated=\'true\''; +} $query = ' SELECT @@ -163,6 +204,7 @@ SELECT c.date, c.author, '.$conf['user_fields']['username'].' AS username, + ui.status, c.content, i.path, i.representative_ext, @@ -173,6 +215,8 @@ SELECT ON i.id = c.image_id LEFT JOIN '.USERS_TABLE.' AS u ON u.'.$conf['user_fields']['id'].' = c.author_id + LEFT JOIN '.USER_INFOS_TABLE.' AS ui + ON ui.user_id = c.author_id WHERE '.implode(' AND ', $where_clauses).' ORDER BY c.date DESC LIMIT '.$page['start'].', '.$conf['comments_page_nb_comments'].' @@ -202,10 +246,12 @@ while ($row = pwg_db_fetch_assoc($result)) 'ID' => $row['id'], 'TN_SRC' => $thumb, 'AUTHOR' => trigger_change('render_comment_author', $author_name), + 'AUTHOR_STATUS' => $row['status'], 'DATE' => format_date($row['date'], array('day_name','day','month','year','time')), 'CONTENT' => trigger_change('render_comment_content',$row['content']), 'IS_PENDING' => ('false' == $row['validated']), 'IP' => $row['anonymous_id'], + 'NUMERICAL_DATE' => $row['date'], ) ); diff --git a/admin/themes/default/template/comments.tpl b/admin/themes/default/template/comments.tpl index 9fda8f142..8ff26389b 100644 --- a/admin/themes/default/template/comments.tpl +++ b/admin/themes/default/template/comments.tpl @@ -1,9 +1,12 @@ +{combine_script id='jquery.ui.slider' require='jquery.ui' load='header' path='themes/default/js/ui/minified/jquery.ui.slider.min.js'} +{combine_css path="themes/default/js/ui/theme/jquery.ui.slider.css"} + {footer_script} jQuery(document).ready(function(){ $("h1").append(""+{$nb_total}+""); function highlighComments() { - jQuery(".checkComment").each(function() { + jQuery(".comment").each(function() { var parent = jQuery(this).parent('tr'); if (jQuery(this).children("input[type=checkbox]").is(':checked')) { jQuery(parent).addClass('selectedComment'); @@ -14,6 +17,103 @@ jQuery(document).ready(function(){ }); } + if ("{$filter}" == "pending"){ + $("#seeWaiting").prop('checked', true); + } + if ("{$filter}" == "validated"){ + $("#seeValidated").prop('checked', true); + } + + $("#seeAll").on("change", function(){ + if ($("#seeAll").prop('checked') == true){ + window.location.replace("{$F_ACTION}&filter=all&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date={$END}"); + } + }); + + $("#seeWaiting").on("change", function(){ + if ($("#seeWaiting").prop('checked') == true){ + window.location.replace("{$F_ACTION}&filter=pending&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date={$END}"); + } + }); + + $("#seeValidated").on("change", function(){ + if ($("#seeValidated").prop('checked') == true){ + window.location.replace("{$F_ACTION}&filter=validated&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date={$END}"); + } + }); + + $("#status_filter").on("change", function(){ + let location = "{$F_ACTION}&filter={$filter}&status=" + $("#status_filter").find(":selected").val().toString() + "&author={$displayed_author}&start_date={$START}&end_date={$END}"; + window.location.replace(location); + }); + + $("#status_filter").val("{$displayed_status}"); + + $("#author_filter").on("change", function(){ + let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author=" + $("#author_filter").find(":selected").val().toString() + "&start_date={$START}&end_date={$END}"; + window.location.replace(location); + }); + + $("#author_filter").val("{$displayed_author}"); + + $("#start_unset").on("click", function(){ + $("#start_date").val(""); + let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date=&end_date={$END}"; + window.location.replace(location); + }); + + $("#start_date").on("focus", function(){ + $(this).data('previous', $(this).val()); + }); + + $("#start_date").val("{$START}".replaceAll("_", "-")); + + $("#start_date").on("change", function(){ + if ($("#end_date").val() != "") + { + var previous = $(this).data('previous'); + var current = new Date($(this).val()); + var max = new Date($("#end_date").val()); + if (current > max){ + $(this).val(previous); + $(this).data('previous', $(this).val()); + return + } + } + $(this).data('previous', $(this).val()); + let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date=" + $(this).val().replaceAll("-", "_") + "&end_date={$END}"; + window.location.replace(location); + }); + + $("#end_unset").on("click", function(){ + $("#end_date").val(""); + let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date="; + window.location.replace(location); + }); + + $("#end_date").on("focus", function(){ + $(this).data('previous', $(this).val()); + }); + + $("#end_date").val("{$END}".replaceAll("_", "-")); + + $("#end_date").on("change", function(){ + if ($("#start_date").val() != "") + { + var previous = $(this).data('previous'); + var current = new Date($(this).val()); + var min = new Date($("#start_date").val()); + if (current < min){ + $(this).val(previous); + $(this).data('previous', $(this).val()); + return + } + } + $(this).data('previous', $(this).val()); + let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date=" + $(this).val().replaceAll("-", "_"); + window.location.replace(location); + }); + jQuery(".checkComment").click(function(event) { var checkbox = jQuery(this).children("input[type=checkbox]"); if (event.target.type !== 'checkbox') { @@ -23,62 +123,271 @@ jQuery(document).ready(function(){ }); jQuery("#commentSelectAll").click(function () { - jQuery(".checkComment input[type=checkbox]").prop('checked', true); + $(".comment-select-checkbox").prop('checked', true); + $(".comment-select-checkbox").trigger("change"); highlighComments(); return false; }); jQuery("#commentSelectNone").click(function () { - jQuery(".checkComment input[type=checkbox]").prop('checked', false); + $(".comment-select-checkbox").prop('checked', false); + $(".comment-select-checkbox").trigger("change"); highlighComments(); return false; }); jQuery("#commentSelectInvert").click(function () { - jQuery(".checkComment input[type=checkbox]").each(function() { + $(".comment-select-checkbox").each(function() { jQuery(this).prop('checked', !$(this).prop('checked')); }); + $(".comment-select-checkbox").trigger("change"); highlighComments(); return false; }); + $(".comment-select-checkbox").on("change", function(event) { + if ($(this).prop("checked")){ + $(this).removeClass("icon-circle-empty") + $(this).addClass("icon-ok-circled") + } + else { + $(this).removeClass("icon-ok-circled") + $(this).addClass("icon-circle-empty") + } + }); + + $("#toggleSelectionMode").on("click", function() { + if ($(".comment-select-checkbox").css("visibility") == "visible") { + $(".comment-buttons-container").css("visibility", "visible"); + $(".comment-select-checkbox").css("visibility", "hidden"); + $(".comment-selection-content").hide(); + $(".comment-container").css("margin-inline-end", "0em") + $("#advanced-filter-menu").css("margin-inline", "23px 10px") + + $(".comment-select-checkbox").prop('checked', false); + $(".comment-select-checkbox").trigger("change"); + highlighComments(); + } + else { + $(".comment-select-checkbox").css("visibility", "visible"); + $(".comment-buttons-container").css("visibility", "hidden"); + $(".comment-selection-content").css("display", "flex") + $(".comment-container").css("margin-inline-end", "5em") + $("#advanced-filter-menu").css("margin-inline", "23px 270px") + } + }) + + $(".advanced-filter-btn").on("click", function() { + if ($("#advanced-filter-menu").css("display") == "none") { + $("#advanced-filter-menu").css("display", "flex") + $("#advanced-filter-menu").css("margin-bottom", "1em") + $(".commentFilter").css("margin-bottom", "0.2em") + $(".commentFilter .advanced-filter-btn").css("height", "100%") + $(".commentFilter .advanced-filter-btn").css("height", "100%") + } + else { + $("#advanced-filter-menu").css("display", "none") + $("#advanced-filter-menu").css("margin-bottom", "0.2em") + $(".commentFilter").css("margin-bottom", "1em") + $(".commentFilter .advanced-filter-btn").css("height", "20px") + } + }) + + if ("{$displayed_status}" != "all" || "{$displayed_author}" != "all" || "{$START}" != "" || "{$END}" != ""){ + $(".advanced-filter-btn").trigger( "click" ); + } + + $(".delete-comment, #commentDeleteSelected").on("click", function() { + jQuery(this).parent().parent().children("input[type=checkbox]").prop('checked', true); + $("#pendingComments").trigger("submit") + }) + + $(".approve-comment, #commentValidateSelected").on("click", function() { + jQuery(this).parent().parent().children("input[type=checkbox]").prop('checked', true); + $("#pendingComments").trigger("submit") + }) + + $("#commentValidateSelected, #commentDeleteSelected").on("click", function() { + $("#pendingComments").trigger("submit") + }) + }); {/footer_script}
- {'All'|@translate} ({$nb_total}) - | {'Waiting'|@translate} ({$nb_pending}) -{if !empty($navbar) }{include file='navigation_bar.tpl'|@get_extent:'navbar'}{/if} + +
+ + + +
+ + {if !empty($navbar) }{include file='navigation_bar.tpl'|@get_extent:'navbar'}{/if} + +
+
+ {'Filters'|@translate} + +
+ +
+ + + +
+ + +
+ + Mode sélection +
+ +
+ +
+ + - {if !empty($comments) } -
+ + + - +
{foreach from=$comments item=comment name=comment} -
- - - + {/if} + {/if} + {/if} + {/if} {/foreach} -
- - -
+ {if $displayed_status == "all" or $displayed_status == $comment.AUTHOR_STATUS} + {if $displayed_author == "all" or $comment.AUTHOR == $displayed_author} + {if $START == "" or (date_timestamp_get(DateTime::createFromFormat("Y-m-d H:i:s", $comment.NUMERICAL_DATE))) >= (date_timestamp_get(DateTime::createFromFormat("Y_m_d", $START)))} + {if $END == "" or (date_timestamp_get(DateTime::createFromFormat("Y-m-d H:i:s", $comment.NUMERICAL_DATE))) <= (date_timestamp_get(DateTime::createFromFormat("Y_m_d", $END)))} +
+ -

{if $comment.IS_PENDING}{'Waiting'|@translate} - {/if}{if !empty($comment.IP)}{$comment.IP} - {/if}{$comment.AUTHOR} - {$comment.DATE}

-
{$comment.CONTENT}
+ +
+ +
" {$comment.CONTENT} "
+ + {if $comment.AUTHOR_STATUS == "webmaster"} + + {elseif $comment.AUTHOR_STATUS == "admin"} + + {elseif $comment.AUTHOR_STATUS == "normal"} + + {elseif $comment.AUTHOR_STATUS == "guest"} + + {/if} + {$comment.AUTHOR} + +

{$comment.DATE}

+ +
+ {if $comment.IS_PENDING} + + {/if} + +
+
+
-
+ +
+ +
+ + + {'Select:'|@translate} {$nb_total} + + + {'All'|@translate} + {'Invert'|@translate} +

- {'Select:'|@translate} - {'All'|@translate}, - {'None'|@translate}, - {'Invert'|@translate} + {'Action:'|@translate}

+ - -
- +
{/if} diff --git a/admin/themes/default/theme.css b/admin/themes/default/theme.css index 6041109d7..a3ca37c07 100644 --- a/admin/themes/default/theme.css +++ b/admin/themes/default/theme.css @@ -26,8 +26,7 @@ ul.categoryActions { margin: 0 2px; width: auto; list-style-position:outside; .content div.comment blockquote { margin-right: 0.5em; overflow: visible; /*avoid a very strange margin behaviour (all browsers) */ } -.commentFilterSelected {color:#666;text-decoration:underline;} -.comment .pendingFlag {font-style:italic;color:red;} +.comment .pendingFlag {font-style:italic;color:red;margin-left: 0.5em;} /* not used but should be */ #thePopuphelpPage .content { margin: 1em; } @@ -547,6 +546,265 @@ LI.menuLi { transform: translate(calc(-50% - 4px), -50%); } +.comment-container{ + display: flex; + flex-flow: row wrap; + align-items: center; + + gap: 1.5em; +} + +.commentFilter{ + margin-bottom: 1em; + padding-inline: 1.5em; + display: flex; + flex-direction: row; +} + +.commentFilter .commentFilter { + margin-inline-start: auto; + gap: 1em; +} + +.commentFilter .advanced-filter-btn{ + height: 20px; + padding: 10px 10px 5px 10px; + margin: 4px; + position: relative; + left: 3.17em; +} + +.advanced-filter-date{ + display: flex; + flex-direction: column; +} + +.advanced-filter-date div { + flex: 1; +} + +.advanced-filter-date div input{ + height: 100%; + width: 74%; + border-color: #D4D4D4; + padding-inline: 1em; +} + +.commentFilter .userActions{ + align-self: center; + z-index: 99; +} + +.commentFilter .pluginTypeFilter{ + transform: none; + position: unset; + margin: 4px; +} + +.commentFilter #search-comment{ + padding-inline-end: 4em; +} + +#search-comment .search-icon{ + position: relative; + left: 37px; + top: 2px; +} + +.comment-selection-content{ + background-color: rgb(250, 250, 250); + border-left: 1px solid rgb(230, 230, 230); + position: absolute; + right: 0px; + width: 223px; + min-height: calc(87% - 171px); + top: 169.5px; + z-index: 10; + padding: 7em 1em 0em 1em; + display: none; + flex-direction: column; +} + +.selectButton { + text-align: center; + background-color: #f0f0f0; + padding: 10px; + color: #777; + font-weight: bold; + margin: 4px; + border-radius: 5px; +} + +.selectButton:hover { + background-color: #ddd; + color: #3A3A3A; + text-decoration: none !important; +} + +.selectButton2 { + text-align: center; + padding: 10px; + color: #3C3C3C; + font-weight: bold; + margin: 4px; + border: solid; + border-width: 1px; + border-color: #E7E7E7; +} + +.selectButton2:hover { + background-color: #ddd; + color: #000000; + text-decoration: none !important; +} + +.comment-form{ + display : flex; +} + +.comment-box-validated, .comment-box-validated:focus{ + border-color: #FFC17E !important; +} + +.comment-box, .comment-box:focus{ + background-color: #FAFAFA; + + display: flex; + flex-direction: row; + align-items: center; + + box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 5px; + + border-radius: 4px; + border-left: solid; + border-color: #999999; + border-width: 6px; + + width: 29.8em; + height: 11.7em; + + padding: 0em 1em 0em 0em; +} + +.comment-box a img{ + object-fit: cover; + height: 11.7em; + width: 11.7em; + margin-top: 0.3em; + margin-right: 3%; +} + +.comment-box .comment{ + display: flex; + flex-direction: column; + height: 100%; + + color: #3C3C3C; + font-weight: 600; +} + +.comment-box .comment blockquote{ + align-self: flex-start; + font-style: italic; + margin : -1em 2% 0% 0%; + align-self: flex-start; + min-height: 0em; +} + +.comment-box .comment strong, .comment-box .comment p{ + align-self: flex-end; + text-align: right; + margin: 0% 2% 0% 0%; +} + +.badge-grey{ + padding-inline : 5px; + color: #898989; + background-color: #DBDBDB; + border-radius: 20px; +} +.badge-red{ + color: #FF5252; + background-color: #FFCFCF; + border-radius: 20px; +} +.badge-guest{ + color: #2883C3; + background-color: #CFEBFF; + border-radius: 20px; +} +.badge-user{ + color: #FFA646; + background-color: #FFE9CF; + border-radius: 20px; +} +.badge-admin{ + color: #896AF3; + background-color: #E0DAF4; + border-radius: 20px; +} +.badge-main-user{ + color: #896AF3; + background-color: #E0DAF4; + border-radius: 20px; +} + +.comment-select-checkbox, .comment-select-checkbox:focus{ + appearance: none; + visibility: hidden; + outline: none; + + position: relative; + top: 5px; + right: -85%; + width: 7%; + height: 10%; + color: #FFA646; + font-size: 2em; + background-color: #FAFAFA; +} + +.comment div { + text-align: end; + min-height: 18%; + min-width: 100%; + margin-top: auto; + margin-bottom: 0.8em; +} + +.comment-buttons-container{ + visibility: visible; +} + +.delete-comment{ + border-width: 0px; + border-radius: 6px; + background-color: #EEEEEE; + color: #3C3C3C; + height: 100%; + font-weight: bold; + font-size: 12px; +} +.delete-comment:hover{ + cursor: pointer; + text-decoration: none; +} + +.approve-comment{ + border-width: 0px; + border-radius: 6px; + background-color: #FFC17E; + color : #3C3C3C; + height: 100%; + font-size: 12px; + font-weight: bold; + margin-right: 3%; +} +.approve-comment:hover{ + cursor: pointer; + text-decoration: none; +} + .cat-modify-content { display: grid; grid-template-rows: 70px 380px; @@ -812,7 +1070,7 @@ LI.menuLi { /* Search bar */ .search-input{ - padding: 10px; + padding: 10px 10px 6px 10px; box-shadow: 0px 2px #00000024; border: none; background-color: #fafafa !important; @@ -2570,6 +2828,10 @@ div.jGrowl div.jGrowl-notification{ margin-left: 5px; } +.advanced-filter-item{ + text-align: left; +} + .advanced-filter-revision-date .advanced-filter-item-label { display: inline !important; @@ -4859,7 +5121,6 @@ a#showPermissions:hover {text-decoration: none;} margin-left: 25px; } -.commentFilter {text-align:left;margin:5px 1em;} FORM#categoryOrdering p.albumTitle {margin:0; margin-left: 5px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; margin-bottom: 4px;} FORM#categoryOrdering p.albumTitle a {font-size: 14px; font-weight: 600;} @@ -6575,6 +6836,23 @@ fieldset#environment legend i[class*="icon-"] { color:#493C21; } +.buttonSecondary{ + font-size:12px; + border:none; + padding:13px 20px; + margin-left:0; font-weight: bold; transition: all 125ms ease-out; + color:#3C3C3C; + background-color:#ECECEC; +} + +.buttonSecondary:hover{ + cursor: pointer; + background-color: #FFA646; + text-decoration: none; + color:#3C3C3C; +} + + #cboxLoadedContent input[type="submit"] {margin-bottom: 20px; float: none;} #permissions, #uploadForm fieldset {border: 0;} @@ -6597,7 +6875,7 @@ fieldset#environment legend i[class*="icon-"] { .icon-plus.cboxElement:hover {box-shadow: 0 2px 4px rgba(0, 0, 0, 0.16);} #albumSelection {margin-right: 20px;} .selectize-control.single .selectize-input {border-radius: 0; font-weight: bold;font-size: 12px;} -.buttonLike {padding: 8px 10px; margin-left: 5px;} +.buttonLike, .buttonSecondary {padding: 8px 10px; margin-left: 5px;} .changeUsername .buttonLike {padding: 1px 10px;} .changePassword .buttonLike {padding: 1px 10px;} .selectFilesButtonBlock {display: flex; margin-top: 10px;} diff --git a/admin/themes/roma/theme.css b/admin/themes/roma/theme.css index ea0521ae5..6ddea9e9a 100644 --- a/admin/themes/roma/theme.css +++ b/admin/themes/roma/theme.css @@ -2394,6 +2394,64 @@ ul.jqtree-tree li.jqtree-ghost span.jqtree-line { background-color:#444; } +.comment-box, .comment-box:focus{ + background-color: #333; + color: #777; + border-color: #777; +} + +.comment-box .comment{ + color : #777; +} + +.comment-selection-content{ + background-color: rgb(51, 51, 51); + border-left: 1px solid #3A3A3A; +} + +.badge-grey{ + background-color: rgb(63, 63, 63); + color: rgb(186, 186, 186); +} + +.selectButton{ + background-color: rgb(85, 85, 85); + color: rgb(193, 193, 193); +} + +.selectButton2{ + border-color: rgb(85, 85, 85); + color: #C1C1C1; +} + +.delete-comment{ + background-color: rgb(85, 85, 85); + color: rgb(193, 193, 193); +} + +.comment-select-checkbox, .comment-select-checkbox:focus{ + background-color: #333; +} + +.commentFilterSelected, .commentFilterSelected:hover { + color: #FFFFFF; + background-color: #FFA500; + border-bottom: solid #D18800; +} +.commentFilterUnselected, .commentFilterUnselected:hover { + color: #F3F3F3; + background-color: #898989; + border-bottom: solid #DBDBDB; +} + +.buttonSecondary { + background-color: #2E2E2E; + color: #777777; +} +.buttonSecondary:hover { + background-color: #1B1B1B; + color: #777777; + /* Filters options */ .select-views{ background-color: #444444; From 2502a44832efd6c37ea35dcdd538272331eee186 Mon Sep 17 00:00:00 2001 From: Linty Date: Mon, 11 Aug 2025 10:57:12 +0200 Subject: [PATCH 011/149] issue #2355 hide api keys warning block by default --- themes/standard_pages/theme.css | 1 + 1 file changed, 1 insertion(+) diff --git a/themes/standard_pages/theme.css b/themes/standard_pages/theme.css index c6635cd4a..6f972bc12 100644 --- a/themes/standard_pages/theme.css +++ b/themes/standard_pages/theme.css @@ -10,6 +10,7 @@ html{ #theHeader, #copyright, #api_custom_date, +#cant_manage_api, #retrieves_keyapi, .template-section, .template-api, From 818233cd5ec398df6d4221e967c0e5e818ad84c3 Mon Sep 17 00:00:00 2001 From: plegall Date: Mon, 11 Aug 2025 14:39:00 +0200 Subject: [PATCH 012/149] bug fixed: getActivityList, use the same SQL where clause to calculate max_line and fetch lines --- include/ws_functions/pwg.php | 103 +++++++++++------------------------ 1 file changed, 32 insertions(+), 71 deletions(-) diff --git a/include/ws_functions/pwg.php b/include/ws_functions/pwg.php index bbb041180..76dd64eb4 100644 --- a/include/ws_functions/pwg.php +++ b/include/ws_functions/pwg.php @@ -478,52 +478,63 @@ function ws_getActivityList($param, &$service) $max = date_format(date_create($param['date_max']), "Y-m-d 23:59:59"); } - if (isset($param['uid'])) { - $query = ' - SELECT - count(*) - FROM '.ACTIVITY_TABLE.' - WHERE object != \'system\' + $where = 'WHERE object != \'system\''; + + if (isset($param['uid'])) + { + $where .= ' AND performed_by = '.$param['uid']; - } else { - $query = ' - SELECT - count(*) - FROM '.ACTIVITY_TABLE.' - WHERE object != \'system\''; } if (isset($param['action'])) { - $query .= ' + $where .= ' AND action = "'.$param['action'].'"'; } if (isset($param['object'])) { - $query .= ' + $where .= ' AND object = "'.$param['object'].'"'; } if (!empty($param['date_min'])) { - $query .= ' + $where .= ' AND occured_on >= "'.$min.'"'; } if (!empty($param['date_max'])) { - $query .= ' + $where .= ' AND occured_on <= "'.$max.'"'; } if (!empty($param['id'])) { - $query .= ' + $where .= ' AND object_id = '.$param['id']; } - $query .= ';'; + if ('none' == $conf['activity_display_connections']) + { + $where .= ' + AND action NOT IN (\'login\', \'logout\')'; + } + elseif ('admins_only' == $conf['activity_display_connections']) + { + include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); + $where .= ' + AND NOT (action IN (\'login\', \'logout\') AND object_id NOT IN ('.implode(',', get_admins()).'))'; + } + + $query = ' +SELECT + count(*) + FROM '.ACTIVITY_TABLE.' + '.$where.' +;'; + //echo $query."\n"; $max_line = (pwg_db_fetch_row(pwg_query($query))[0]); @@ -542,61 +553,11 @@ SELECT details, user_agent FROM '.ACTIVITY_TABLE.' - WHERE object != \'system\''; - - if (isset($param['uid'])) - { - $query.= ' - AND performed_by = '.$param['uid']; - } - - if (isset($param['action'])) - { - $query .= ' - AND action = "'.$param['action'].'"'; - } - - if (isset($param['object'])) - { - $query .= ' - AND object = "'.$param['object'].'"'; - } - - if (!empty($param['date_min'])) - { - $query .= ' - AND occured_on >= "'.$min.'"'; - } - - if (!empty($param['date_max'])) - { - $query .= ' - AND occured_on <= "'.$max.'"'; - } - - if (!empty($param['id'])) - { - $id = pwg_db_real_escape_string(stripslashes($param['id'])); - $query .= ' - AND object_id = '.$id; - } - - elseif ('none' == $conf['activity_display_connections']) - { - $query.= ' - AND action NOT IN (\'login\', \'logout\')'; - } - elseif ('admins_only' == $conf['activity_display_connections']) - { - include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); - $query.= ' - AND NOT (action IN (\'login\', \'logout\') AND object_id NOT IN ('.implode(',', get_admins()).'))'; - } - - $query.= ' + '.$where.' ORDER BY activity_id DESC - LIMIT '.($page_size * 5).' OFFSET '.$page_offset.' + LIMIT '.($page_size * 10).' OFFSET '.$page_offset.' ;'; +// echo $query."\n"; $result = pwg_query($query); From 4e04ee0f22f2e3deffea98e2a9b0a17e699cdc63 Mon Sep 17 00:00:00 2001 From: Linty Date: Mon, 11 Aug 2025 15:23:12 +0200 Subject: [PATCH 013/149] issue #2386 fix activity link in admin group list --- admin/group_list.php | 2 -- admin/themes/default/template/group_list.tpl | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/admin/group_list.php b/admin/group_list.php index fbdb8e832..1cdcde2b2 100644 --- a/admin/group_list.php +++ b/admin/group_list.php @@ -102,8 +102,6 @@ SELECT u.'. $conf['user_fields']['username'].' AS username ) ); - $template->assign('U_ACTIVITY', get_root_url().'admin.php?page=user_activity&group='.$row['id']); - $group_counter++; } diff --git a/admin/themes/default/template/group_list.tpl b/admin/themes/default/template/group_list.tpl index 39e159e2c..707a290da 100644 --- a/admin/themes/default/template/group_list.tpl +++ b/admin/themes/default/template/group_list.tpl @@ -87,7 +87,7 @@ usersCache.selectize(jQuery('select.UserSearch')); - {'Activity'|@translate} + {'Activity'|@translate}
From 5d9dcb9e5f1c328fef0847e81a7a0fa80e1c75e9 Mon Sep 17 00:00:00 2001 From: plegall Date: Tue, 12 Aug 2025 15:55:23 +0200 Subject: [PATCH 014/149] issue #2386 optimize load of min/max date --- .../themes/default/template/user_activity.tpl | 12 +++---- admin/user_activity.php | 35 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/admin/themes/default/template/user_activity.tpl b/admin/themes/default/template/user_activity.tpl index 4e5ba1f70..59bf6b809 100644 --- a/admin/themes/default/template/user_activity.tpl +++ b/admin/themes/default/template/user_activity.tpl @@ -176,9 +176,9 @@ var actionInfos_tags_moved = "{'%d tags moved'|translate}"; class="activity-date-selecter" type="date" id="date_min_activity" - value={$ACTIVITY_DATES.min} - min={$ACTIVITY_DATES.min} - max={$ACTIVITY_DATES.max} + value="{$ACTIVITY_DATES.min}" + min="{$ACTIVITY_DATES.min}" + max="{$ACTIVITY_DATES.max}" />
@@ -188,9 +188,9 @@ var actionInfos_tags_moved = "{'%d tags moved'|translate}"; class="activity-date-selecter" type="date" id="date_max_activity" - value={$ACTIVITY_DATES.max} - min={$ACTIVITY_DATES.min} - max={$ACTIVITY_DATES.max} + value="{$ACTIVITY_DATES.max}" + min="{$ACTIVITY_DATES.min}" + max="{$ACTIVITY_DATES.max}" /> diff --git a/admin/user_activity.php b/admin/user_activity.php index f62d34014..8d782c284 100644 --- a/admin/user_activity.php +++ b/admin/user_activity.php @@ -146,24 +146,27 @@ $query = ' SELECT occured_on FROM '.ACTIVITY_TABLE.' - WHERE object != \'system\' - ;'; + ORDER BY activity_id ASC + LIMIT 1 +;'; +list($min_date) = pwg_db_fetch_row(pwg_query($query)); -$result = query2array($query); +$query = ' +SELECT + occured_on + FROM '.ACTIVITY_TABLE.' + ORDER BY activity_id DESC + LIMIT 1 +;'; +list($max_date) = pwg_db_fetch_row(pwg_query($query)); -$dates = array(); - -foreach($result as $time){ - list($date, $hour) = explode(' ', $time['occured_on']); - $dates[] = date_format(date_create($date),"Y-m-d"); -} - -$dates = array_unique($dates); -$list_dates['allDates'] = implode(',',$dates); -$list_dates['min'] = $dates[0]; -$list_dates['max'] = end($dates); - -$template->assign('ACTIVITY_DATES', $list_dates); +$template->assign( + 'ACTIVITY_DATES', + array( + 'min' => empty($min_date) ? '' : substr($min_date, 0, 10), + 'max' => empty($max_date) ? '' : substr($max_date, 0, 10), + ) +); $additional_filt_type = false; $additional_filt_name = null; From 09a03d9818601f910570ed759ee0340ca000d3bc Mon Sep 17 00:00:00 2001 From: plegall Date: Tue, 12 Aug 2025 16:03:32 +0200 Subject: [PATCH 015/149] issue #2386 optimize pwg.activity.getList * fetch block of 10k rows from activity table instead of 500. It's not much slower and may avoid many loops to generate 100 lines of output aggregated lines. * no need to perform a slow query to count the total number of filtered rows, we can just detect if there are more rows to aggregate to know if we have reached the last page. * parse details only if we're on a new output key (which is discutable, but we use details only on new output line creation) --- include/ws_functions/pwg.php | 70 +++++++++++++++++------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/include/ws_functions/pwg.php b/include/ws_functions/pwg.php index 76dd64eb4..69e58c117 100644 --- a/include/ws_functions/pwg.php +++ b/include/ws_functions/pwg.php @@ -462,13 +462,12 @@ function ws_getActivityList($param, &$service) $page_size = 100; //We will fetch X lines in database =/= lines displayed due to line concatenation //$page_offset = $param['page']*$page_size; $page_offset = $param['offset']; + $nb_rows_to_fetch = 10000; $user_ids = array(); $line_id = 0; - - if (!empty($param['date_min'])) { $min = date_format(date_create($param['date_min']), "Y-m-d H:i:s"); $max = date_format(date_create($param['date_max']), "Y-m-d 23:59:59"); @@ -528,17 +527,9 @@ function ws_getActivityList($param, &$service) AND NOT (action IN (\'login\', \'logout\') AND object_id NOT IN ('.implode(',', get_admins()).'))'; } - $query = ' -SELECT - count(*) - FROM '.ACTIVITY_TABLE.' - '.$where.' -;'; - //echo $query."\n"; - - $max_line = (pwg_db_fetch_row(pwg_query($query))[0]); + $more_rows_available = true; - while(sizeof($output_lines) < $page_size and !($page_offset >= $max_line)) + while (count($output_lines) < $page_size and $more_rows_available) { $query = ' SELECT @@ -555,36 +546,21 @@ SELECT FROM '.ACTIVITY_TABLE.' '.$where.' ORDER BY activity_id DESC - LIMIT '.($page_size * 10).' OFFSET '.$page_offset.' + LIMIT '.$nb_rows_to_fetch.' OFFSET '.$page_offset.' ;'; -// echo $query."\n"; + $rows = query2array($query); - $result = pwg_query($query); - - while ($row = pwg_db_fetch_assoc($result)) + if (count($rows) < $nb_rows_to_fetch) { - if (sizeof($output_lines) < $page_size) + $more_rows_available = false; + } + + foreach ($rows as $row) + { + if (count($output_lines) < $page_size) { $page_offset++; - $row['details'] = str_replace('`groups`', 'groups', $row['details']); - $row['details'] = str_replace('`rank`', 'rank', $row['details']); - $details = @unserialize($row['details']); - - if (isset($row['user_agent'])) - { - $details['agent'] = $row['user_agent']; - } - - if (isset($details['method'])) - { - $detailsType = 'method'; - } - if (isset($details['script'])) - { - $detailsType = 'script'; - } - $line_key = $row['session_idx'].'~'.$row['object'].'~'.$row['action'].'~'; // idx~photo~add if ($line_key === $current_key) @@ -595,6 +571,25 @@ SELECT } else { + $row['details'] = str_replace('`groups`', 'groups', $row['details']); + $row['details'] = str_replace('`rank`', 'rank', $row['details']); + $details = @unserialize($row['details']); + + if (isset($row['user_agent'])) + { + $details['agent'] = $row['user_agent']; + } + + if (isset($details['method'])) + { + $detailsType = 'method'; + } + + if (isset($details['script'])) + { + $detailsType = 'script'; + } + list($date, $hour) = explode(' ', $row['occured_on']); // New line $output_lines[] = array( @@ -623,6 +618,7 @@ SELECT } else { + $more_rows_available = true; break; } } @@ -667,7 +663,7 @@ SELECT return array( 'result_lines' => $output_lines, 'page_offset' => $page_offset, - 'end_page' => ($page_offset >= $max_line), + 'end_page' => !$more_rows_available, 'params' => $param ); } From b8fcc216b8dd4de25f7c8997eebbc13305dc02db Mon Sep 17 00:00:00 2001 From: plegall Date: Tue, 12 Aug 2025 16:20:25 +0200 Subject: [PATCH 016/149] issue #2386 simplify/secure additional filters --- admin/user_activity.php | 59 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/admin/user_activity.php b/admin/user_activity.php index 8d782c284..3ed83adaa 100644 --- a/admin/user_activity.php +++ b/admin/user_activity.php @@ -16,8 +16,13 @@ include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); // +-----------------------------------------------------------------------+ // | Check Access and exit when user status is not ok | // +-----------------------------------------------------------------------+ + check_status(ACCESS_ADMINISTRATOR); +check_input_parameter('photo', $_GET, false, PATTERN_ID); +check_input_parameter('album', $_GET, false, PATTERN_ID); +check_input_parameter('group', $_GET, false, PATTERN_ID); + // +-----------------------------------------------------------------------+ // | tabs | // +-----------------------------------------------------------------------+ @@ -172,41 +177,35 @@ $additional_filt_type = false; $additional_filt_name = null; $additional_filt_value = null; -if(isset($_GET['photo'])) -{ - $query = ' - SELECT - name - FROM '.IMAGES_TABLE.' - WHERE id = '.$_GET['photo'].';'; +$additional_filters = array( + 'photo' => IMAGES_TABLE, + 'album' => CATEGORIES_TABLE, + 'group' => GROUPS_TABLE, +); - $additional_filt_type = 'photo'; - $additional_filt_name = query2array($query)[0]['name']; - $additional_filt_value = $_GET['photo']; -} -else if (isset($_GET['album'])) +foreach ($additional_filters as $filter_key => $filter_table) { - $query = ' - SELECT + if (isset($_GET[$filter_key])) + { + $query = ' +SELECT name - FROM '.CATEGORIES_TABLE.' - WHERE id = '.$_GET['album'].';'; + FROM '.$filter_table.' + WHERE id = '.$_GET[$filter_key].' +;'; + $rows = query2array($query); - $additional_filt_type = 'album'; - $additional_filt_name = query2array($query)[0]['name']; - $additional_filt_value = $_GET['album']; -} -else if (isset($_GET['group'])) -{ - $query = ' - SELECT - name - FROM '.GROUPS_TABLE.' - WHERE id = '.$_GET['group'].';'; + if (count($rows) == 0) + { + fatal_error($filter_key.' #'.$_GET[$filter_key].' does not exist'); + } - $additional_filt_type = 'group'; - $additional_filt_name = query2array($query)[0]['name']; - $additional_filt_value = $_GET['group']; + $additional_filt_type = $filter_key; + $additional_filt_name = $rows[0]['name']; + $additional_filt_value = $_GET[$filter_key]; + + break; + } } $template->assign('ADDITIONAL_FILT', array( From 61961bd1729544f65811c409455367540b2eab2b Mon Sep 17 00:00:00 2001 From: Linty Date: Tue, 12 Aug 2025 16:49:54 +0200 Subject: [PATCH 017/149] issue #2386 reset pagination on date filter change --- admin/themes/default/js/user_activity.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/admin/themes/default/js/user_activity.js b/admin/themes/default/js/user_activity.js index 8333177de..7667e48f4 100644 --- a/admin/themes/default/js/user_activity.js +++ b/admin/themes/default/js/user_activity.js @@ -652,6 +652,7 @@ $(document).ready(function () { }); $('#date_min_activity').on('change', function(user) { + page_reset(); if ($('#date_min_activity').val()=='') { document.getElementById('date_max_activity').setAttribute("min", date_min); @@ -664,6 +665,7 @@ $(document).ready(function () { }) $('#date_max_activity').on('change', function(user) { + page_reset(); if ($('#date_max_activity').val()=='') { document.getElementById('date_min_activity').setAttribute("max", date_max); From 66df2096321ecf15bfb16914cb37271d69d3998f Mon Sep 17 00:00:00 2001 From: RushLana Date: Fri, 16 May 2025 09:39:49 +0200 Subject: [PATCH 018/149] Change graphic library priority, ext_imagemick is now preffered --- admin/include/functions.php | 16 ++++++++-------- admin/include/image.class.php | 10 +++++----- admin/maintenance_actions.php | 22 +++++++++++----------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/admin/include/functions.php b/admin/include/functions.php index 654ce4515..638629887 100644 --- a/admin/include/functions.php +++ b/admin/include/functions.php @@ -3726,6 +3726,14 @@ function get_graphics_library() switch (pwg_image::get_library()) { + case 'ext_imagick': + exec($conf['ext_imagick_dir'].'convert -version', $returnarray); + if (preg_match('/Version: ImageMagick (\d+\.\d+\.\d+-?\d*)/', $returnarray[0], $match)) + { + $library.= '/'.$match[1]; + } + break; + case 'imagick': $img = new Imagick(); $version = $img->getVersion(); @@ -3735,14 +3743,6 @@ function get_graphics_library() } break; - case 'ext_imagick': - exec($conf['ext_imagick_dir'].'convert -version', $returnarray); - if (preg_match('/Version: ImageMagick (\d+\.\d+\.\d+-?\d*)/', $returnarray[0], $match)) - { - $library.= '/'.$match[1]; - } - break; - case 'gd': $gd_info = gd_info(); $library.= '/'.@$gd_info['GD Version']; diff --git a/admin/include/image.class.php b/admin/include/image.class.php index 9ae2350cd..0b17ef5f1 100644 --- a/admin/include/image.class.php +++ b/admin/include/image.class.php @@ -406,16 +406,16 @@ class pwg_image switch (strtolower($library)) { case 'auto': - case 'imagick': - if ($extension != 'gif' and self::is_imagick()) - { - return 'imagick'; - } case 'ext_imagick': if ($extension != 'gif' and self::is_ext_imagick()) { return 'ext_imagick'; } + case 'imagick': + if ($extension != 'gif' and self::is_imagick()) + { + return 'imagick'; + } case 'gd': if (self::is_gd()) { diff --git a/admin/maintenance_actions.php b/admin/maintenance_actions.php index c001a2a1d..a15dba72f 100644 --- a/admin/maintenance_actions.php +++ b/admin/maintenance_actions.php @@ -318,17 +318,6 @@ $template->assign( // graphics library switch (pwg_image::get_library()) { - case 'imagick': - $library = 'ImageMagick'; - $img = new Imagick(); - $version = $img->getVersion(); - if (preg_match('/ImageMagick \d+\.\d+\.\d+-?\d*/', $version['versionString'], $match)) - { - $library = $match[0]; - } - $template->assign('GRAPHICS_LIBRARY', $library); - break; - case 'ext_imagick': $library = 'External ImageMagick'; exec($conf['ext_imagick_dir'].'convert -version', $returnarray); @@ -339,6 +328,17 @@ switch (pwg_image::get_library()) $template->assign('GRAPHICS_LIBRARY', $library); break; + case 'imagick': + $library = 'ImageMagick'; + $img = new Imagick(); + $version = $img->getVersion(); + if (preg_match('/ImageMagick \d+\.\d+\.\d+-?\d*/', $version['versionString'], $match)) + { + $library = $match[0]; + } + $template->assign('GRAPHICS_LIBRARY', $library); + break; + case 'gd': $gd_info = gd_info(); $template->assign('GRAPHICS_LIBRARY', 'GD '.@$gd_info['GD Version']); From 3293b329e916b743c56f786227a1be6c47f0f4c9 Mon Sep 17 00:00:00 2001 From: RushLana Date: Mon, 19 May 2025 09:09:10 +0200 Subject: [PATCH 019/149] Implement imagick command fix `pwg_image::get_ext_imagick_command()` get the imagick command (convert or magick) replace all convert with pwg_image::get_ext_imagick_command() --- admin/include/functions.php | 2 +- admin/include/functions_upload.inc.php | 10 ++++----- admin/include/image.class.php | 28 ++++++++++++++++++++++++-- admin/maintenance_actions.php | 2 +- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/admin/include/functions.php b/admin/include/functions.php index 638629887..90cd7e567 100644 --- a/admin/include/functions.php +++ b/admin/include/functions.php @@ -3727,7 +3727,7 @@ function get_graphics_library() switch (pwg_image::get_library()) { case 'ext_imagick': - exec($conf['ext_imagick_dir'].'convert -version', $returnarray); + exec($conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command().' -version', $returnarray); if (preg_match('/Version: ImageMagick (\d+\.\d+\.\d+-?\d*)/', $returnarray[0], $match)) { $library.= '/'.$match[1]; diff --git a/admin/include/functions_upload.inc.php b/admin/include/functions_upload.inc.php index 46e0fdaf9..1f0fa325b 100644 --- a/admin/include/functions_upload.inc.php +++ b/admin/include/functions_upload.inc.php @@ -590,7 +590,7 @@ function upload_file_pdf($representative_ext, $file_path) $representative_file_path = original_to_representative($file_path, $ext); prepare_directory(dirname($representative_file_path)); - $exec = $conf['ext_imagick_dir'].'convert'; + $exec = $conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command(); if ('jpg' == $ext) { $exec.= ' -quality '.$jpg_quality; @@ -639,7 +639,7 @@ function upload_file_heic($representative_ext, $file_path) list($w,$h) = get_optimal_dimensions_for_representative(); - $exec = $conf['ext_imagick_dir'].'convert'; + $exec = $conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command(); $exec.= ' -sampling-factor 4:2:0 -quality 85 -interlace JPEG -colorspace sRGB -auto-orient +repage -resize "'.$w.'x'.$h.'>"'; $exec.= ' "'.realpath($file_path).'"'; $exec.= ' "'.$representative_file_path.'"'; @@ -689,7 +689,7 @@ function upload_file_tiff($representative_ext, $file_path) prepare_directory(dirname($representative_file_path)); - $exec = $conf['ext_imagick_dir'].'convert'; + $exec = $conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command(); if ('jpg' == $conf['tiff_representative_ext']) { @@ -836,7 +836,7 @@ function upload_file_psd($representative_ext, $file_path) prepare_directory(dirname($representative_file_path)); - $exec = $conf['ext_imagick_dir'].'convert'; + $exec = $conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command(); $exec .= ' "'.realpath($file_path).'"'; @@ -898,7 +898,7 @@ function upload_file_eps($representative_ext, $file_path) // convert -density 300 image.eps -resize 2048x2048 image.png - $exec = $conf['ext_imagick_dir'].'convert'; + $exec = $conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command(); $exec.= ' -density 300'; $exec.= ' "'.realpath($file_path).'"'; $exec.= ' -resize 2048x2048'; diff --git a/admin/include/image.class.php b/admin/include/image.class.php index 0b17ef5f1..ace71bc3b 100644 --- a/admin/include/image.class.php +++ b/admin/include/image.class.php @@ -368,6 +368,29 @@ class pwg_image return (extension_loaded('imagick') and class_exists('Imagick')); } + static function get_ext_imagick_command() + { + global $page, $conf; + + if (!isset($page['ext_imagick_command'])) + { + $retval=null; + $cmd_out=null; + // check if magick is in path + exec('command -v '.$conf['ext_imagick_dir'].'magick', $cmd_out , $retval ); + if (0 == $retval) + { + $page['ext_imagick_command'] = $cmd_out[0]; + } + else + { + $page['ext_imagick_command'] = $conf['ext_imagick_dir'].'convert'; + } + } + + return $page['ext_imagick_command']; + } + static function is_ext_imagick() { global $conf; @@ -376,7 +399,8 @@ class pwg_image { return false; } - @exec($conf['ext_imagick_dir'].'convert -version', $returnarray); + + @exec($conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command().' -version', $returnarray); if (is_array($returnarray) and !empty($returnarray[0]) and preg_match('/ImageMagick/i', $returnarray[0])) { if (preg_match('/Version: ImageMagick (\d+\.\d+\.\d+-?\d*)/', $returnarray[0], $match)) @@ -704,7 +728,7 @@ class image_ext_imagick implements imageInterface $this->add_command('sampling-factor', '4:2:2' ); } - $exec = $this->imagickdir.'convert'; + $exec = $this->imagickdir.pwg_image::get_ext_imagick_command(); $exec .= ' "'.realpath($this->source_filepath).'"'; foreach ($this->commands as $command => $params) diff --git a/admin/maintenance_actions.php b/admin/maintenance_actions.php index a15dba72f..ce4c6b842 100644 --- a/admin/maintenance_actions.php +++ b/admin/maintenance_actions.php @@ -320,7 +320,7 @@ switch (pwg_image::get_library()) { case 'ext_imagick': $library = 'External ImageMagick'; - exec($conf['ext_imagick_dir'].'convert -version', $returnarray); + exec($conf['ext_imagick_dir'].pwg_image::get_ext_imagick_command().' -version', $returnarray); if (preg_match('/Version: ImageMagick (\d+\.\d+\.\d+-?\d*)/', $returnarray[0], $match)) { $library .= ' ' . $match[1]; From c616f171458149dba680a0aca3a6f96693d9f7b3 Mon Sep 17 00:00:00 2001 From: RushLana Date: Wed, 21 May 2025 14:47:12 +0200 Subject: [PATCH 020/149] Fix webp animation derivatives Using imagick to generate derivates on an animated webp break animation Applying "-layers coalesce" ensure the animation remain smooth Refs : - https://github.com/ImageMagick/ImageMagick/issues/6375 - https://github.com/ImageMagick/ImageMagick/issues/5542 - https://github.com/ImageMagick/ImageMagick/issues/4246 --- admin/include/image.class.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/admin/include/image.class.php b/admin/include/image.class.php index ace71bc3b..2b1119e3f 100644 --- a/admin/include/image.class.php +++ b/admin/include/image.class.php @@ -731,6 +731,12 @@ class image_ext_imagick implements imageInterface $exec = $this->imagickdir.pwg_image::get_ext_imagick_command(); $exec .= ' "'.realpath($this->source_filepath).'"'; + // If the image is animated webp add a filter to avoid breaking the animation + if ($this->is_animated_webp) + { + $exec .= ' -layers coalesce '; + } + foreach ($this->commands as $command => $params) { $exec .= ' -'.$command; @@ -739,7 +745,6 @@ class image_ext_imagick implements imageInterface $exec .= ' '.$params; } } - $dest = pathinfo($destination_filepath); $exec .= ' "'.realpath($dest['dirname']).'/'.$dest['basename'].'" 2>&1'; $logger->debug($exec, 'i.php'); From 93cc0e72a85954d557a835faf6de6149f59d3e3d Mon Sep 17 00:00:00 2001 From: Linty Date: Tue, 26 Aug 2025 14:30:24 +0200 Subject: [PATCH 021/149] update toaster template class and fix toast timeout --- themes/standard_pages/js/toaster.js | 4 ++-- themes/standard_pages/template/toaster.tpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/themes/standard_pages/js/toaster.js b/themes/standard_pages/js/toaster.js index 1d1396a54..af6656cfe 100644 --- a/themes/standard_pages/js/toaster.js +++ b/themes/standard_pages/js/toaster.js @@ -20,11 +20,11 @@ function pwgToaster(info) { template.find('.toast_icon').addClass(info.icon === 'success' ? 'icon-ok' : 'icon-cancel'); template.addClass(info.icon === 'success' ? info.icon : 'error'); - template.removeClass('template'); + template.removeClass('template-pwg-toaster'); template.appendTo('#pwg_toaster'); const time = info.time ?? 3600; - setInterval(() => { + setTimeout(() => { template.fadeOut(() => { template.remove(); }) diff --git a/themes/standard_pages/template/toaster.tpl b/themes/standard_pages/template/toaster.tpl index a7e731192..7a2fd37a1 100644 --- a/themes/standard_pages/template/toaster.tpl +++ b/themes/standard_pages/template/toaster.tpl @@ -1,6 +1,6 @@ {combine_script id='toaster_js' load='async' require='jquery' path='themes/standard_pages/js/toaster.js'} {html_style} -.toast.template { +.toast.template-pwg-toaster { display: none; } @@ -52,7 +52,7 @@ } {/html_style}
-
+

From 57042cc475712736ccdb8707e47aea67aa217065 Mon Sep 17 00:00:00 2001 From: Linty Date: Tue, 26 Aug 2025 14:35:31 +0200 Subject: [PATCH 022/149] fixes #2364 redesign admin comments management Replaces legacy PHP comment management with a new interface for listing, filtering, selecting, validating, and deleting user comments. Updates templates and CSS for a modern, interactive experience, adds advanced filters, selection mode, and modal comment viewing. Removes obsolete server-side logic from comments.php and introduces new api methods for comment actions. --- admin/comments.php | 229 +------ admin/themes/clear/theme.css | 9 +- admin/themes/default/js/comments.js | 590 ++++++++++++++++ admin/themes/default/template/comments.tpl | 583 ++++++---------- admin/themes/default/theme.css | 738 ++++++++++++--------- admin/themes/roma/theme.css | 133 ++-- include/ws_functions/pwg.comments.php | 243 +++++++ language/en_UK/admin.lang.php | 3 + language/fr_FR/admin.lang.php | 3 + ws.php | 79 +++ 10 files changed, 1606 insertions(+), 1004 deletions(-) create mode 100644 admin/themes/default/js/comments.js create mode 100644 include/ws_functions/pwg.comments.php diff --git a/admin/comments.php b/admin/comments.php index cb2f4a7d2..133b545de 100644 --- a/admin/comments.php +++ b/admin/comments.php @@ -13,70 +13,12 @@ if (!defined('PHPWG_ROOT_PATH')) include_once(PHPWG_ROOT_PATH.'admin/include/functions.php'); -if (isset($_GET['start']) and is_numeric($_GET['start'])) -{ - $page['start'] = $_GET['start']; -} -else -{ - $page['start'] = 0; -} - // +-----------------------------------------------------------------------+ // | Check Access and exit when user status is not ok | // +-----------------------------------------------------------------------+ check_status(ACCESS_ADMINISTRATOR); -// +-----------------------------------------------------------------------+ -// | actions | -// +-----------------------------------------------------------------------+ - -if (!empty($_POST)) -{ - if (empty($_POST['comments'])) - { - $template->assign( - array( - 'save_warning' => l10n('Select at least one comment') - ) - ); - } - else - { - include_once( PHPWG_ROOT_PATH .'include/functions_comment.inc.php' ); - check_input_parameter('comments', $_POST, true, PATTERN_ID); - - if (isset($_POST['validate'])) - { - validate_user_comment($_POST['comments']); - - $template->assign( - array( - 'save_success' => l10n_dec( - '%d user comment validated', '%d user comments validated', - count($_POST['comments']) - ) - ) - ); - } - - if (isset($_POST['reject'])) - { - delete_user_comment($_POST['comments']); - - $template->assign( - array( - 'save_error' => l10n_dec( - '%d user comment rejected', '%d user comments rejected', - count($_POST['comments']) - ) - ) - ); - } - } -} - // +-----------------------------------------------------------------------+ // | template init | // +-----------------------------------------------------------------------+ @@ -85,7 +27,8 @@ $template->set_filenames(array('comments'=>'comments.tpl')); $template->assign( array( - 'F_ACTION' => get_root_url().'admin.php?page=comments' + 'F_ACTION' => get_root_url().'admin.php?page=comments', + 'PWG_TOKEN' => get_pwg_token(), ) ); @@ -102,174 +45,6 @@ $tabsheet->set_id('comments'); $tabsheet->select(''); $tabsheet->assign(); -// +-----------------------------------------------------------------------+ -// | comments display | -// +-----------------------------------------------------------------------+ - -$nb_total = 0; -$nb_pending = 0; -$nb_validated = 0; - -$query = ' -SELECT - COUNT(*) AS counter, - validated - FROM '.COMMENTS_TABLE.' - GROUP BY validated -;'; -$result = pwg_query($query); -while ($row = pwg_db_fetch_assoc($result)) -{ - $nb_total+= $row['counter']; - - if ('false' == $row['validated']) - { - $nb_pending = $row['counter']; - } -} - -$nb_validated = $nb_total - $nb_pending; - -if (!isset($_GET['filter']) and $nb_pending > 0) -{ - $page['filter'] = 'pending'; -} -else -{ - $page['filter'] = 'all'; -} - -if (isset($_GET['filter']) and ('pending' == $_GET['filter'] or 'validated' == $_GET['filter'])) -{ - $page['filter'] = $_GET['filter']; -} - -if (isset($_GET['status'])) -{ - $displayed_status = $_GET['status']; -} -else -{ - $displayed_status = 'all'; -} - -if (isset($_GET['author'])) -{ - $author = $_GET['author']; -} -else -{ - $author = 'all'; -} - -// by default, no filter by date is active -$start = $end = ""; - -if (isset($_GET['start_date'])){ - $start = $_GET['start_date']; -} - -if (isset($_GET['end_date'])){ - $end = $_GET['end_date']; -} - -$template->assign( - array( - 'nb_total' => $nb_total, - 'nb_pending' => $nb_pending, - 'nb_validated' => $nb_validated, - 'filter' => $page['filter'], - 'displayed_status' => $displayed_status, - 'displayed_author' => $author, - 'START' => $start, - 'END' => $end, - ) - ); - -$where_clauses = array('1=1'); - -if ('pending' == $page['filter']) -{ - $where_clauses[] = 'validated=\'false\''; -} -if ('validated' == $page['filter']) -{ - $where_clauses[] = 'validated=\'true\''; -} - -$query = ' -SELECT - c.id, - c.image_id, - c.date, - c.author, - '.$conf['user_fields']['username'].' AS username, - ui.status, - c.content, - i.path, - i.representative_ext, - validated, - c.anonymous_id - FROM '.COMMENTS_TABLE.' AS c - INNER JOIN '.IMAGES_TABLE.' AS i - ON i.id = c.image_id - LEFT JOIN '.USERS_TABLE.' AS u - ON u.'.$conf['user_fields']['id'].' = c.author_id - LEFT JOIN '.USER_INFOS_TABLE.' AS ui - ON ui.user_id = c.author_id - WHERE '.implode(' AND ', $where_clauses).' - ORDER BY c.date DESC - LIMIT '.$page['start'].', '.$conf['comments_page_nb_comments'].' -;'; -$result = pwg_query($query); -while ($row = pwg_db_fetch_assoc($result)) -{ - $thumb = DerivativeImage::thumb_url( - array( - 'id'=>$row['image_id'], - 'path'=>$row['path'], - 'representative_ext'=>$row['representative_ext'], - ) - ); - if (empty($row['author_id'])) - { - $author_name = $row['author']; - } - else - { - $author_name = stripslashes($row['username']); - } - $template->append( - 'comments', - array( - 'U_PICTURE' => get_root_url().'admin.php?page=photo-'.$row['image_id'], - 'ID' => $row['id'], - 'TN_SRC' => $thumb, - 'AUTHOR' => trigger_change('render_comment_author', $author_name), - 'AUTHOR_STATUS' => $row['status'], - 'DATE' => format_date($row['date'], array('day_name','day','month','year','time')), - 'CONTENT' => trigger_change('render_comment_content',$row['content']), - 'IS_PENDING' => ('false' == $row['validated']), - 'IP' => $row['anonymous_id'], - 'NUMERICAL_DATE' => $row['date'], - ) - ); - - $list[] = $row['id']; -} - -// +-----------------------------------------------------------------------+ -// | navigation bar | -// +-----------------------------------------------------------------------+ - -$navbar = create_navigation_bar( - get_root_url().'admin.php'.get_query_string_diff(array('start')), - ('pending' == $page['filter'] ? $nb_pending : $nb_total), - $page['start'], - $conf['comments_page_nb_comments'] - ); - -$template->assign('navbar', $navbar); $template->assign('ADMIN_PAGE_TITLE', l10n('User comments')); // +-----------------------------------------------------------------------+ diff --git a/admin/themes/clear/theme.css b/admin/themes/clear/theme.css index ed5b8b726..e7a4f494a 100644 --- a/admin/themes/clear/theme.css +++ b/admin/themes/clear/theme.css @@ -481,7 +481,8 @@ input:focus + .slider { box-shadow: 0 0 1px #ffa646; } -#selection-mode-block{ +#selection-mode-block, +.selection-mode { background-color: #FAFAFA; border-left:1px solid #e6e6e6; } @@ -505,12 +506,14 @@ input:focus + .slider { color:#a0a0a0; } -#selection-mode-block button{ +#selection-mode-block button, +#commentsSelection button{ border: 1px solid #e7e7e7; } -#selection-mode-block button:hover{ +#selection-mode-block button:hover, +#commentsSelection button:hover { background-color: #ffa744; border: 1px solid #ffa744; } diff --git a/admin/themes/default/js/comments.js b/admin/themes/default/js/comments.js new file mode 100644 index 000000000..f7254c1bb --- /dev/null +++ b/admin/themes/default/js/comments.js @@ -0,0 +1,590 @@ +const commentsContainer = $('#comments'); +const advancedFilters = $('#advancedFilters'); +const switchMode = $('#toggleSelectionMode'); +const commentContainer = $('#commentContainer'); +const commentsAll = $('#commentsAll'); +const commentsValidated = $('#commentsValidated'); +const commentsPending = $('#commentsPending'); +const commentsList = $('#commentsList'); +const commentsNb = $('#commentsNb a'); +const filterAuthor = $('#filter_author'); +const filterDateStart = $('#filter_date_start'); +const filterDateEnd = $('#filter_date_end'); +const commentsSelectController = $('#commentsSelectController'); +const tabFilters = $('#tabFilters'); +const commentsSelectedArea = $('#commentsSelected'); +const commentsSelectedOthers = $('#commentsSelectedOthers'); +const modalViewComment = $('#modalViewComment'); + +const commentsPaginElipsis = '...'; +const commentsPaginItems = '%d'; +const commentsPaginItemsCurrent = '%d'; +const commentsOptionsFiltersAuthor = ''; +const commentsSelectedList = '

#%d

'; + +let commentsState = {}; +let commentsParams = { + status: 'all', + page: 0, + per_page: 5, +} + +let updateAuthorId = true; +let searchTimeOut = null; +let selectionMode = false; +let commentsSelected = []; + +$(function() { + $('#commentFilters').on('click', function() { + $(this).toggleClass('advanced-filter-open'); + advancedFilters.toggle(); + }); + + switchMode.on('change', function() { + $('#contentSelectMode').toggle(); + $('#headerSelectMode, #contentSelectMode').toggleClass('selection-mode'); + commentContainer.toggleClass('active'); + + if (!commentContainer.hasClass('active')) { + selectionMode = false; + $('.comment-select-checkbox').hide(); + + $('.comment-buttons').show(); + commentsSelectController.removeClass('show'); + tabFilters.show(); + commentsUnselectAll(); + } else { + selectionMode = true; + $('.comment-select-checkbox').show(); + + $('.comment-buttons').hide(); + tabFilters.hide(); + commentsSelectController.addClass('show'); + } + }); + + $('#selectAll').on('click', function() { + commentsSelectAll(); + }); + + $('#selectNone').on('click', function() { + commentsUnselectAll(); + }); + + $('#selectInvert').on('click', function() { + commentsInvertSelect(); + }); + + $('.tab-filters input').on('change', function() { + commentsParams.status = $(this).attr('data-status'); + commentsParams.page = 0; + getComments(commentsParams); + }); + + commentsNb.on('click', function() { + const nb = $(this).text(); + updateNbComments(nb); + commentsParams.page = 0; + getComments(commentsParams); + }); + + $('#closeModalViewComment').on('click', function() { + closeModalViewComment(); + }); + + $('#commentSearchInput').on('input', function() { + clearTimeout(searchTimeOut); + searchTimeOut = setTimeout(() => { + const search = $(this).val(); + + delete commentsParams.author_id; + delete commentsParams.f_min_date; + delete commentsParams.f_max_date; + + commentsParams.search = search; + getComments(commentsParams); + }, 300); + }); + + $('#commentsResetFilters').on('click', function() { + commentsClearFilters(); + }); + + // get comments and set display + commentsParams.per_page = window.localStorage.getItem('adminCommentsNB') ?? 5 + updateNbComments(commentsParams.per_page); + getComments(commentsParams); +}); + + +function getComments(params) { + $.ajax({ + url: 'ws.php?format=json&method=pwg.userComments.getList', + type: 'GET', + dataType: 'json', + data: params, + success: (data) => { + if (data.stat === 'ok') { + // for debug + // console.log(data.result); + commentsState = {...data.result}; + commentsDisplaySummary(data.result.summary); + displayComments(data.result.comments); + commentsDiplayPagination(data.result.paging); + commentsDisplayFilters(data.result.filters); + + delete commentsParams.search; + } + }, + error: (e) => { + console.log(e); + $.alert({ title: str_an_error_has, content: "" , ...jConfirm_warning_options}); + } + }) +} + +function commentsDisplaySummary(summary) { + commentsAll.text(summary.all_comments); + commentsValidated.text(summary.validated); + commentsPending.text(summary.pending); +} + +function displayComments(comments) { + commentsList.empty(); + comments.forEach((comment) => { + const clone = $('.comment-template').clone(); + clone.removeClass('comment-template').addClass('comment'); + + clone.attr('id', comment.id); + clone.find('.comment-img').attr('src', comment.medium_url); + const raw_lenght = comment.raw_content.length; + const preview = raw_lenght > 50 ? comment.raw_content.substring(0, 50) + '...' : comment.raw_content; + clone.find('.comment-msg').text('"' + preview + '"'); + clone.find('.comment-author-name').text(comment.author); + clone.find('.comment-datetime').text(comment.date); + clone.find('.comment-delete').data('idx', comment.id); + clone.find('.comment-validate').data('idx', comment.id); + clone.find('.comment-content').data('idx', comment.id); + clone.find('.comment-hash').text(`#${comment.id}`); + clone.find('.comment-select-checkbox').val(comment.id); + clone.find('.comment-link').attr('href', comment.admin_link); + const authorIcons = clone.find('.comment-author-icon'); + + switch (comment.author_status) { + case "guest": + authorIcons.addClass('icon-user-secret icon-yellow'); + break; + + case "webmaster": + authorIcons.addClass('icon-user icon-purple'); + break; + + case "admin": + authorIcons.addClass('icon-user icon-green'); + break; + + case "main_user": + authorIcons.addClass('icon-king icon-blue'); + break; + + default: + authorIcons.addClass('icon-user icon-yellow'); + break; + } + + if (comment.is_pending) { + clone.find('.comment-validate').show(); + } else { + clone.find('.comment-container').addClass('comment-validated'); + } + + commentsList.append(clone); + }); + + $('.comment-delete').off('click').on('click', function(e) { + e.stopPropagation(); + const id = $(this).data('idx'); + deleteComment([id]); + }); + + $('.comment-validate').off('click').on('click', function(e) { + e.stopPropagation(); + const id = $(this).data('idx'); + validateComment([id]); + }); + + $('.comment-content').off('click').on('click', function() { + const id = $(this).data('idx'); + if (selectionMode) { + const checkbox = $(this).find('.comment-select-checkbox'); + + if (checkbox.hasClass('icon-circle-empty')) { + checkbox.removeClass('icon-circle-empty').addClass('icon-ok-circled'); + $(`#${id}`).addClass('comment-selected'); + commentsSelected.push(id); + + } else { + checkbox.removeClass('icon-ok-circled').addClass('icon-circle-empty'); + $(`#${id}`).removeClass('comment-selected'); + + commentsSelected = commentsSelected.filter((idx) => idx != id); + } + + commentsUpdateSelection(); + return; + } + + showModalViewComment(id); + }); +} + +function commentsDiplayPagination(paging) { + const container = $('.pagination-item-container'); + container.empty(); + + if (paging.total_pages == 0) { + const pageNumbers = paging.total_pages + 1; + const page = commentsPaginItems.replace(/%d/g, pageNumbers); + $(page).addClass('actual').appendTo(container); + + } else if (paging.total_pages <= 2) { + Array.from(Array(paging.total_pages + 1)).forEach((_, i) => { + const page = commentsPaginItems.replace(/%d/g, i + 1); + $(page).appendTo(container); + }); + $(`#comments_page_${paging.page + 1}`).addClass('actual'); + + } else { + const pageOne = commentsPaginItems.replace(/%d/g, 1); + const pageLast = commentsPaginItems.replace(/%d/g, paging.total_pages + 1); + const pageCurrent = commentsPaginItemsCurrent.replace(/%d/g, paging.page + 1); + + switch (paging.page) { + case 0: + container.append([ + pageCurrent, + commentsPaginElipsis, + pageLast + ]); + break; + + case paging.total_pages: + container.append([ + pageOne, + commentsPaginElipsis, + pageCurrent + ]); + break; + + default: + container.append([ + pageOne, + commentsPaginElipsis, + pageCurrent, + commentsPaginElipsis, + pageLast + ]); + break; + } + + $('.pagination-arrow').removeClass('unavailable') + .off('click').on('click', function() { + let newPage = commentsParams.page; + if ($(this).hasClass('left')) { + newPage = newPage - 1; + } else { + newPage = newPage + 1; + } + + if (newPage == -1 || newPage > commentsState.paging.total_pages) { + return; + } + commentsParams.page = newPage; + getComments(commentsParams); + }); + } + + $('.comments-paging').off('click').on('click', function() { + const newPage = $(this).attr('data-page') - 1; + commentsParams.page = newPage; + getComments(commentsParams); + }); + + +} + +function commentsDisplayFilters(filters) { + if (updateAuthorId) { + commentsDisplayAuthors(filters.nb_authors); + } + // reset here to let decide filterAuthor onChange + updateAuthorId = true; + + const minDate = filters.started_at.split(' ')[0] ?? ''; + const maxDate = filters.ended_at.split(' ')[0] ?? '' + filterDateStart.val(minDate).attr({ 'min': minDate, 'max': maxDate }); + filterDateEnd.val(maxDate).attr({ 'max': maxDate, 'min': minDate }); + + + filterDateStart.off('change').on('change', function() { + const min = $(this).val(); + + if (!min) { + delete commentsParams.f_min_date; + } else { + commentsParams.f_min_date = min; + } + + filterDateEnd.attr({ 'min': min }); + commentsParams.page = 0; + getComments(commentsParams); + }); + + filterDateEnd.off('change').on('change', function() { + const max = $(this).val(); + + if (!max) { + delete commentsParams.f_max_date; + } else { + commentsParams.f_max_date = max; + } + + filterDateStart.attr({ 'max': max }); + commentsParams.page = 0; + getComments(commentsParams); + }); +} + +function commentsDisplayAuthors(nb_authors) { + filterAuthor.empty(); + filterAuthor.append(commentsOptionsFiltersAuthor); + + nb_authors.forEach((a) => { + filterAuthor.append(` + + `); + }); + + filterAuthor.off('change').on('change', function() { + const authorId = $(this).val(); + + if (!authorId) { + delete commentsParams.author_id; + } else { + commentsParams.author_id = authorId; + } + + commentsParams.page = 0; + updateAuthorId = false; + getComments(commentsParams); + }); +} + +function updateNbComments(nb) { + commentsNb.removeClass('selected-pagination'); + $(`#pagination-per-page-${nb}`).addClass('selected-pagination'); + + commentsParams.per_page = nb; + window.localStorage.setItem('adminCommentsNB', nb); +} + +function showModalViewComment(id) { + const comment = commentsState.comments.filter((c) => c.id == id)[0] ?? null; + if (!comment) return; + + const item = $(`#${id}`); + modalViewComment.find('.comment-datetime').text(comment.date); + modalViewComment.find('.comment-author').remove(); + modalViewComment.find('.comments-modal-infos').prepend(item.find('.comment-author').clone()); + modalViewComment.find('.comments-modal-img').attr('src', comment.medium_url); + modalViewComment.find('.comments-modal-img-i').empty() + .append(` +

${comment.file}

+

${comment.image_date_available}

+ `); + modalViewComment.find('.comments-modal-body').html(comment.content) + + modalViewComment.fadeIn(); +} + +function closeModalViewComment() { + modalViewComment.fadeOut(); +} + +function validateComment(id) { + $.ajax({ + url: 'ws.php?format=json&method=pwg.userComments.validate', + type: 'POST', + dataType: 'json', + data: { + comment_id: id, + pwg_token: pwg_token + }, + success: function (res) { + if (res.stat === 'ok') { + $.alert({ + ...{ + title: str_comment_validated, + content: "", + }, + ...jConfirm_alert_options + }); + getComments(commentsParams); + return; + } + $.alert({ + ...{ + title: str_an_error_has, + content: "", + }, + ...jConfirm_warning_options + }); + }, + error: function (e) { + console.log(e) + $.alert({ + ...{ + title: str_an_error_has, + content: "", + }, + ...jConfirm_warning_options + }); + } + }); +} + +function deleteComment(id) { + $.confirm({ + title: str_delete.replace("%s", id), + draggable: false, + titleClass: "jconfirmDeleteConfirm", + theme: "modern", + content: "", + animation: "zoom", + boxWidth: '30%', + useBootstrap: false, + type: 'red', + animateFromElement: false, + backgroundDismiss: true, + typeAnimated: false, + buttons: { + confirm: { + text: str_yes_delete_confirmation, + btnClass: 'btn-red', + action: function () { + $.ajax({ + url: 'ws.php?format=json&method=pwg.userComments.delete', + type: 'POST', + dataType: 'json', + data: { + comment_id: id, + pwg_token + }, + success: function(res) { + if (res.stat === 'ok') { + getComments(commentsParams); + } + }, + error: function(e) { + console.log(e) + } + }) + } + }, + cancel: { + text: str_no_delete_confirmation + } + } + }); +} + +function commentsUnselectAll() { + $('.comment').removeClass('comment-selected'); + $('.comment-select-checkbox') + .removeClass('icon-ok-circled') + .addClass('icon-circle-empty'); + + commentsSelected = []; + commentsUpdateSelection(); +} + +function commentsSelectAll(){ + $('.comment').addClass('comment-selected'); + $('.comment-select-checkbox') + .removeClass('icon-circle-empty') + .addClass('icon-ok-circled'); + + commentsSelected = []; + $('.comment-selected').each((i, el) => { + const id = $(el).attr('id'); + commentsSelected.push(id); + }); + commentsUpdateSelection(); +} + +function commentsInvertSelect() { + $('.comment').toggleClass('comment-selected'); + $('.comment-select-checkbox') + .toggleClass('icon-ok-circled') + .toggleClass('icon-circle-empty'); + + commentsSelected = []; + $('.comment-selected').each((i, el) => { + const id = $(el).attr('id'); + commentsSelected.push(id); + }); + commentsUpdateSelection(); +} + +function commentsUpdateSelection() { + if (commentsSelected.length === 0) { + $('#commentsSelection').hide(); + $('#commentsNoSelection').show(); + $('.comments-selected-remove').off('click'); + $('#ValisateSelectionMode').off('click'); + $('#DeleteSelectionMode').off('click'); + + return; + } + + commentsSelectedArea.empty(); + let count = 0; + commentsSelected.forEach((id) => { + if (count === 5) { + commentsSelectedOthers.text(str_and_others.replace(/%s/g, commentsSelected.length - 5)); + return; + } + commentsSelectedOthers.text(''); + const item = commentsSelectedList.replace(/%d/g, id); + commentsSelectedArea.append(item); + count++ + }); + + $('.comments-selected-remove').off('click').on('click', function() { + const id = $(this).attr('id').split('_')[1]; + if (!id) return; + $(`#${id} .comment-content`).trigger('click'); + }); + + $('#ValisateSelectionMode').off('click').on('click', function() { + validateComment(commentsSelected); + commentsUnselectAll(); + }); + + $('#DeleteSelectionMode').off('click').on('click', function() { + deleteComment(commentsSelected); + commentsUnselectAll(); + }); + + $('#commentsNoSelection').hide(); + $('#commentsSelection').show(); +} + +function commentsClearFilters() { + delete commentsParams.author_id; + delete commentsParams.image_id; + delete commentsParams.search; + delete commentsParams.f_min_date; + delete commentsParams.f_max_date; + getComments(commentsParams); +} \ No newline at end of file diff --git a/admin/themes/default/template/comments.tpl b/admin/themes/default/template/comments.tpl index 8ff26389b..de680e204 100644 --- a/admin/themes/default/template/comments.tpl +++ b/admin/themes/default/template/comments.tpl @@ -1,422 +1,209 @@ -{combine_script id='jquery.ui.slider' require='jquery.ui' load='header' path='themes/default/js/ui/minified/jquery.ui.slider.min.js'} -{combine_css path="themes/default/js/ui/theme/jquery.ui.slider.css"} - +{combine_script id="comments" load="footer" path="admin/themes/default/js/comments.js"} +{combine_script id='common' load='footer' path='admin/themes/default/js/common.js'} +{combine_script id='jquery.confirm' load='footer' require='jquery' path='themes/default/js/plugins/jquery-confirm.min.js'} +{combine_css path="themes/default/js/plugins/jquery-confirm.min.css"} {footer_script} -jQuery(document).ready(function(){ - $("h1").append(""+{$nb_total}+""); - - function highlighComments() { - jQuery(".comment").each(function() { - var parent = jQuery(this).parent('tr'); - if (jQuery(this).children("input[type=checkbox]").is(':checked')) { - jQuery(parent).addClass('selectedComment'); - } - else { - jQuery(parent).removeClass('selectedComment'); - } - }); - } - - if ("{$filter}" == "pending"){ - $("#seeWaiting").prop('checked', true); - } - if ("{$filter}" == "validated"){ - $("#seeValidated").prop('checked', true); - } - - $("#seeAll").on("change", function(){ - if ($("#seeAll").prop('checked') == true){ - window.location.replace("{$F_ACTION}&filter=all&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date={$END}"); - } - }); - - $("#seeWaiting").on("change", function(){ - if ($("#seeWaiting").prop('checked') == true){ - window.location.replace("{$F_ACTION}&filter=pending&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date={$END}"); - } - }); - - $("#seeValidated").on("change", function(){ - if ($("#seeValidated").prop('checked') == true){ - window.location.replace("{$F_ACTION}&filter=validated&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date={$END}"); - } - }); - - $("#status_filter").on("change", function(){ - let location = "{$F_ACTION}&filter={$filter}&status=" + $("#status_filter").find(":selected").val().toString() + "&author={$displayed_author}&start_date={$START}&end_date={$END}"; - window.location.replace(location); - }); - - $("#status_filter").val("{$displayed_status}"); - - $("#author_filter").on("change", function(){ - let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author=" + $("#author_filter").find(":selected").val().toString() + "&start_date={$START}&end_date={$END}"; - window.location.replace(location); - }); - - $("#author_filter").val("{$displayed_author}"); - - $("#start_unset").on("click", function(){ - $("#start_date").val(""); - let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date=&end_date={$END}"; - window.location.replace(location); - }); - - $("#start_date").on("focus", function(){ - $(this).data('previous', $(this).val()); - }); - - $("#start_date").val("{$START}".replaceAll("_", "-")); - - $("#start_date").on("change", function(){ - if ($("#end_date").val() != "") - { - var previous = $(this).data('previous'); - var current = new Date($(this).val()); - var max = new Date($("#end_date").val()); - if (current > max){ - $(this).val(previous); - $(this).data('previous', $(this).val()); - return - } - } - $(this).data('previous', $(this).val()); - let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date=" + $(this).val().replaceAll("-", "_") + "&end_date={$END}"; - window.location.replace(location); - }); - - $("#end_unset").on("click", function(){ - $("#end_date").val(""); - let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date="; - window.location.replace(location); - }); - - $("#end_date").on("focus", function(){ - $(this).data('previous', $(this).val()); - }); - - $("#end_date").val("{$END}".replaceAll("_", "-")); - - $("#end_date").on("change", function(){ - if ($("#start_date").val() != "") - { - var previous = $(this).data('previous'); - var current = new Date($(this).val()); - var min = new Date($("#start_date").val()); - if (current < min){ - $(this).val(previous); - $(this).data('previous', $(this).val()); - return - } - } - $(this).data('previous', $(this).val()); - let location = "{$F_ACTION}&filter={$filter}&status={$displayed_status}&author={$displayed_author}&start_date={$START}&end_date=" + $(this).val().replaceAll("-", "_"); - window.location.replace(location); - }); - - jQuery(".checkComment").click(function(event) { - var checkbox = jQuery(this).children("input[type=checkbox]"); - if (event.target.type !== 'checkbox') { - jQuery(checkbox).prop('checked', !jQuery(checkbox).prop('checked')); - } - highlighComments(); - }); - - jQuery("#commentSelectAll").click(function () { - $(".comment-select-checkbox").prop('checked', true); - $(".comment-select-checkbox").trigger("change"); - highlighComments(); - return false; - }); - - jQuery("#commentSelectNone").click(function () { - $(".comment-select-checkbox").prop('checked', false); - $(".comment-select-checkbox").trigger("change"); - highlighComments(); - return false; - }); - - jQuery("#commentSelectInvert").click(function () { - $(".comment-select-checkbox").each(function() { - jQuery(this).prop('checked', !$(this).prop('checked')); - }); - $(".comment-select-checkbox").trigger("change"); - highlighComments(); - return false; - }); - - $(".comment-select-checkbox").on("change", function(event) { - if ($(this).prop("checked")){ - $(this).removeClass("icon-circle-empty") - $(this).addClass("icon-ok-circled") - } - else { - $(this).removeClass("icon-ok-circled") - $(this).addClass("icon-circle-empty") - } - }); - - $("#toggleSelectionMode").on("click", function() { - if ($(".comment-select-checkbox").css("visibility") == "visible") { - $(".comment-buttons-container").css("visibility", "visible"); - $(".comment-select-checkbox").css("visibility", "hidden"); - $(".comment-selection-content").hide(); - $(".comment-container").css("margin-inline-end", "0em") - $("#advanced-filter-menu").css("margin-inline", "23px 10px") - - $(".comment-select-checkbox").prop('checked', false); - $(".comment-select-checkbox").trigger("change"); - highlighComments(); - } - else { - $(".comment-select-checkbox").css("visibility", "visible"); - $(".comment-buttons-container").css("visibility", "hidden"); - $(".comment-selection-content").css("display", "flex") - $(".comment-container").css("margin-inline-end", "5em") - $("#advanced-filter-menu").css("margin-inline", "23px 270px") - } - }) - - $(".advanced-filter-btn").on("click", function() { - if ($("#advanced-filter-menu").css("display") == "none") { - $("#advanced-filter-menu").css("display", "flex") - $("#advanced-filter-menu").css("margin-bottom", "1em") - $(".commentFilter").css("margin-bottom", "0.2em") - $(".commentFilter .advanced-filter-btn").css("height", "100%") - $(".commentFilter .advanced-filter-btn").css("height", "100%") - } - else { - $("#advanced-filter-menu").css("display", "none") - $("#advanced-filter-menu").css("margin-bottom", "0.2em") - $(".commentFilter").css("margin-bottom", "1em") - $(".commentFilter .advanced-filter-btn").css("height", "20px") - } - }) - - if ("{$displayed_status}" != "all" || "{$displayed_author}" != "all" || "{$START}" != "" || "{$END}" != ""){ - $(".advanced-filter-btn").trigger( "click" ); - } - - $(".delete-comment, #commentDeleteSelected").on("click", function() { - jQuery(this).parent().parent().children("input[type=checkbox]").prop('checked', true); - $("#pendingComments").trigger("submit") - }) - - $(".approve-comment, #commentValidateSelected").on("click", function() { - jQuery(this).parent().parent().children("input[type=checkbox]").prop('checked', true); - $("#pendingComments").trigger("submit") - }) - - $("#commentValidateSelected, #commentDeleteSelected").on("click", function() { - $("#pendingComments").trigger("submit") - }) - -}); +const str_yes_delete_confirmation = "{'Yes, delete'|@translate|@escape:'javascript'}" +const str_no_delete_confirmation = "{"No, I have changed my mind"|@translate|@escape:'javascript'}" +const str_delete = "{'Are you sure you want to delete comment "%s"?'|@translate|@escape:'javascript'}" +const str_no_comments_selected = "{'No comments selected, no actions possible.'|@translate|@escape:'javascript'}" +const pwg_token = "{$PWG_TOKEN}" +const str_an_error_has = "{"An error has occured"|@translate|@escape:'javascript'}" +const str_comment_validated = "{"The comment has been validated."|@translate|@escape:'javascript'}" +const str_and_others = "{"and %s others"|@translate}" {/footer_script} + +
+
+
+
+ + -
+ + -
- - - -
+ + +
- {if !empty($navbar) }{include file='navigation_bar.tpl'|@get_extent:'navbar'}{/if} +
+

{'Select'|@translate}

+

{'All'|@translate}

+

{'None'|@translate}

+

{'Invert'|@translate}

+
-
-
- {'Filters'|@translate} - -
- -
- - - -
- - -
- - Mode sélection -
- -
- -
- - - -{/if} +
+ +
+ +
+
+ +
+
+
+

+ +
+
+ +

+
+
+ +

+
+
+ + +
+
+
\ No newline at end of file diff --git a/admin/themes/default/theme.css b/admin/themes/default/theme.css index a3ca37c07..3a554f448 100644 --- a/admin/themes/default/theme.css +++ b/admin/themes/default/theme.css @@ -11,22 +11,6 @@ ul.categoryActions { margin: 0 2px; width: auto; list-style-position:outside; padding: 0; text-indent: 0; list-style: none; text-align: center; } .content div.titrePage { padding: 0 0 3px; } -.content div.comment { margin: 0 0 0.5em 0; padding: 0; - overflow: hidden; width: 100%; /* don't ask why. It's a very usefull trick */ } - -.content DIV.comment A.illustration { - display: block; - float: left; - margin: 0.5em 30px 0 0.5em; -} - -.content div.comment p.commentHeader { - text-align: right; margin: 0.5em 0.5em 0 0; } -.content div.comment ul.actions { text-align: center; margin: 0.2em; } -.content div.comment blockquote { - margin-right: 0.5em; overflow: visible; /*avoid a very strange margin behaviour (all browsers) */ } - -.comment .pendingFlag {font-style:italic;color:red;margin-left: 0.5em;} /* not used but should be */ #thePopuphelpPage .content { margin: 1em; } @@ -546,265 +530,6 @@ LI.menuLi { transform: translate(calc(-50% - 4px), -50%); } -.comment-container{ - display: flex; - flex-flow: row wrap; - align-items: center; - - gap: 1.5em; -} - -.commentFilter{ - margin-bottom: 1em; - padding-inline: 1.5em; - display: flex; - flex-direction: row; -} - -.commentFilter .commentFilter { - margin-inline-start: auto; - gap: 1em; -} - -.commentFilter .advanced-filter-btn{ - height: 20px; - padding: 10px 10px 5px 10px; - margin: 4px; - position: relative; - left: 3.17em; -} - -.advanced-filter-date{ - display: flex; - flex-direction: column; -} - -.advanced-filter-date div { - flex: 1; -} - -.advanced-filter-date div input{ - height: 100%; - width: 74%; - border-color: #D4D4D4; - padding-inline: 1em; -} - -.commentFilter .userActions{ - align-self: center; - z-index: 99; -} - -.commentFilter .pluginTypeFilter{ - transform: none; - position: unset; - margin: 4px; -} - -.commentFilter #search-comment{ - padding-inline-end: 4em; -} - -#search-comment .search-icon{ - position: relative; - left: 37px; - top: 2px; -} - -.comment-selection-content{ - background-color: rgb(250, 250, 250); - border-left: 1px solid rgb(230, 230, 230); - position: absolute; - right: 0px; - width: 223px; - min-height: calc(87% - 171px); - top: 169.5px; - z-index: 10; - padding: 7em 1em 0em 1em; - display: none; - flex-direction: column; -} - -.selectButton { - text-align: center; - background-color: #f0f0f0; - padding: 10px; - color: #777; - font-weight: bold; - margin: 4px; - border-radius: 5px; -} - -.selectButton:hover { - background-color: #ddd; - color: #3A3A3A; - text-decoration: none !important; -} - -.selectButton2 { - text-align: center; - padding: 10px; - color: #3C3C3C; - font-weight: bold; - margin: 4px; - border: solid; - border-width: 1px; - border-color: #E7E7E7; -} - -.selectButton2:hover { - background-color: #ddd; - color: #000000; - text-decoration: none !important; -} - -.comment-form{ - display : flex; -} - -.comment-box-validated, .comment-box-validated:focus{ - border-color: #FFC17E !important; -} - -.comment-box, .comment-box:focus{ - background-color: #FAFAFA; - - display: flex; - flex-direction: row; - align-items: center; - - box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 5px; - - border-radius: 4px; - border-left: solid; - border-color: #999999; - border-width: 6px; - - width: 29.8em; - height: 11.7em; - - padding: 0em 1em 0em 0em; -} - -.comment-box a img{ - object-fit: cover; - height: 11.7em; - width: 11.7em; - margin-top: 0.3em; - margin-right: 3%; -} - -.comment-box .comment{ - display: flex; - flex-direction: column; - height: 100%; - - color: #3C3C3C; - font-weight: 600; -} - -.comment-box .comment blockquote{ - align-self: flex-start; - font-style: italic; - margin : -1em 2% 0% 0%; - align-self: flex-start; - min-height: 0em; -} - -.comment-box .comment strong, .comment-box .comment p{ - align-self: flex-end; - text-align: right; - margin: 0% 2% 0% 0%; -} - -.badge-grey{ - padding-inline : 5px; - color: #898989; - background-color: #DBDBDB; - border-radius: 20px; -} -.badge-red{ - color: #FF5252; - background-color: #FFCFCF; - border-radius: 20px; -} -.badge-guest{ - color: #2883C3; - background-color: #CFEBFF; - border-radius: 20px; -} -.badge-user{ - color: #FFA646; - background-color: #FFE9CF; - border-radius: 20px; -} -.badge-admin{ - color: #896AF3; - background-color: #E0DAF4; - border-radius: 20px; -} -.badge-main-user{ - color: #896AF3; - background-color: #E0DAF4; - border-radius: 20px; -} - -.comment-select-checkbox, .comment-select-checkbox:focus{ - appearance: none; - visibility: hidden; - outline: none; - - position: relative; - top: 5px; - right: -85%; - width: 7%; - height: 10%; - color: #FFA646; - font-size: 2em; - background-color: #FAFAFA; -} - -.comment div { - text-align: end; - min-height: 18%; - min-width: 100%; - margin-top: auto; - margin-bottom: 0.8em; -} - -.comment-buttons-container{ - visibility: visible; -} - -.delete-comment{ - border-width: 0px; - border-radius: 6px; - background-color: #EEEEEE; - color: #3C3C3C; - height: 100%; - font-weight: bold; - font-size: 12px; -} -.delete-comment:hover{ - cursor: pointer; - text-decoration: none; -} - -.approve-comment{ - border-width: 0px; - border-radius: 6px; - background-color: #FFC17E; - color : #3C3C3C; - height: 100%; - font-size: 12px; - font-weight: bold; - margin-right: 3%; -} -.approve-comment:hover{ - cursor: pointer; - text-decoration: none; -} - .cat-modify-content { display: grid; grid-template-rows: 70px 380px; @@ -1070,7 +795,7 @@ LI.menuLi { /* Search bar */ .search-input{ - padding: 10px 10px 6px 10px; + padding: 10px; box-shadow: 0px 2px #00000024; border: none; background-color: #fafafa !important; @@ -2828,10 +2553,6 @@ div.jGrowl div.jGrowl-notification{ margin-left: 5px; } -.advanced-filter-item{ - text-align: left; -} - .advanced-filter-revision-date .advanced-filter-item-label { display: inline !important; @@ -3201,10 +2922,16 @@ div.jGrowl div.jGrowl-notification{ padding: 13px; } +.tab-filters .filter, .pluginTypeFilter .filter { display: none; } +.tab-filters { + display: flex; + align-items: center; +} + .pluginTypeFilter { display: flex; flex-direction: row; @@ -3217,6 +2944,7 @@ div.jGrowl div.jGrowl-notification{ transform: translateY(13px); } +.tab-filters label, .pluginTypeFilter label { display: flex; flex-direction: row; @@ -3233,6 +2961,7 @@ div.jGrowl div.jGrowl-notification{ font-weight: 600; } +.tab-filters label .filter-badge, .pluginTypeFilter label .filter-badge { font-weight: normal; border-radius: 20px; @@ -3241,17 +2970,20 @@ div.jGrowl div.jGrowl-notification{ margin-left: 10px; } +.tab-filters input:checked + label, .pluginTypeFilter input:checked + label{ background: #ffa500; color: white; box-shadow: 0 2px #d18800; } +.tab-filters input:checked + label .filter-badge, .pluginTypeFilter input:checked + label .filter-badge { background-color: #d18800; color: #ffe5b6; } +.tab-filters input + label .filter-badge, .pluginTypeFilter input + label .filter-badge { background-color: #dbdbdb; color: #8c8c8c; @@ -4448,7 +4180,8 @@ a#showPermissions:hover {text-decoration: none;} cursor: pointer; } -.tag-header .selection-controller { +.tag-header .selection-controller , +.comments-selection-controller { display: none; align-items: baseline; height: 38px; @@ -4456,7 +4189,8 @@ a#showPermissions:hover {text-decoration: none;} font-weight: bold; } -.tag-header .selection-controller.show { +.tag-header .selection-controller.show, +.comments-selection-controller.show { display: flex; } @@ -4465,7 +4199,8 @@ a#showPermissions:hover {text-decoration: none;} color: #555; } -.tag-header .selection-controller a { +.tag-header .selection-controller a, +.comments-selection-controller .comments-selection-btn { margin: 0; background-color: #f0f0f0; padding: 10px; @@ -4475,7 +4210,8 @@ a#showPermissions:hover {text-decoration: none;} border-radius: 5px; } -.tag-header .selection-controller a:hover { +.tag-header .selection-controller a:hover, +.comments-selection-controller .comments-selection-btn:hover { background-color: #ddd; color: #3A3A3A; text-decoration: none; @@ -4701,7 +4437,8 @@ a#showPermissions:hover {text-decoration: none;} text-align: start; } -.selection-mode-tag .selection-other-tags { +.selection-mode-tag .selection-other-tags, +.comments-and-others { color: #ffa646; font-weight: bold; font-size: 15px; @@ -5103,17 +4840,406 @@ a#showPermissions:hover {text-decoration: none;} background-color: #ffd7ad; } -/* Pending Comments */ -#pendingComments { - padding:0 5px 0 10px; +/* Comments */ +.comments { + display: grid; + grid-template-rows: auto 1fr; + min-height: calc(100svh - 169px); + padding-left: 20px; } -#pendingComments table { - width:100%; +.comments p { + margin: 0; } -#pendingComments .bottomButtons { - text-align:left; +.comments-selection, +.comment-validate, +.comments-selections, +.comment-template { + display: none; +} + +.comments-selection-mode, +.comments-selection { + border-left: 1px solid transparent; +} + +.comments-selection { + padding-top: 20px; +} + +.comments-container.active { + display: grid; + grid-template-columns: 1fr 250px; +} + +.comments-container.active .comment-content:hover { + background-color: #FFD9A7; +} + +.comment.comment-selected .comment-content { + background-color: #FFD9A7; +} + +.comments-container { + /* display: flex; */ + width: 100%; +} + +.comments-filters { + display: grid; + grid-template-columns: 1fr 250px; +} + +.comments-filters { + align-items: center; +} + +.comments-selection-mode { + height: 100%; + align-content: center; + text-align: start; + padding-left: 20px; +} + +.comments-selection-switch { + margin-top: 20px; +} + +.comments-search-cancel { + display: none; +} + +.comments-advanced-filter, +.comments-tabs-filters, +.comments-search { + display: flex; + align-items: center; +} + +.comments-advanced-filter { + gap: 10px; +} + +.comments-tabs-filters { + padding: 20px 20px 0 0; + justify-content: space-between; +} + +.comments-selection-btn { + cursor: pointer; +} + +.comments-search { + height: 36px; + padding: 0 5px; + box-shadow: 0px 2px #00000024; + border: none; + background-color: #fafafa !important; + width: 200px; +} + +.comments-search:hover { + background-color: #f0f0f0 !important +} + +.comments-search-icon, +.comments-search-cancel { + font-size: 18px; +} + +.comments-search-icon::before, +.comments-search-cancel::before { + margin-left: 0; +} + +.comments-search input { + all: unset; + background-color: transparent !important; + width: 100%; + height: 100%; + line-height: 18px; + text-align: start; +} + +.advanced-filter-dates-max .advanced-filter-select-container::before, +.advanced-filter-dates-min .advanced-filter-select-container::before { + content: ""; +} + +.advanced-filter-dates-max .advanced-filter-select, +.advanced-filter-dates-min .advanced-filter-select { + padding: 0 10px; + height: 32px; +} + +.comments-advanced-filters .advanced-filter-select { + min-width: 150px; + -webkit-appearance: none; + appearance: none; +} + +.comments-advanced-filters { + display: none; + background-color: #F3F3F3; + margin: 10px 20px 0 0; + position: relative; + z-index: 99; +} + +.comments-filters-container { + display: flex; + align-items: center; + padding: 10px; + gap: 20px; +} + +.comments-filters-date { + height: 33px !important; + padding: 0 5px !important; +} + +.comments-list { + display: flex; + flex-wrap: wrap; + gap: 20px; + padding-top: 20px; + transition: flex-wrap 0.5s ease; +} + +.comment-container { + background-color: #FAFAFA; + display: flex; + flex-direction: row; + align-items: stretch; + box-shadow: rgba(0, 0, 0, 0.14) 0px 2px 5px; + border-radius: 4px; + border-left: 6px solid #FFC17E; + /* width: 29.8em; + height: 11.7em; */ + height: 150px; + width: 390px; + overflow: hidden; +} + +.comment-validated { + border-left: 6px solid #999999 !important; +} + +.comment-content { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 10px; + cursor: pointer; +} + +.comment-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.comment-footer { + display: flex; + align-items: center; + justify-content: space-between; +} + +.comment-hash { + color: #7a7a7a89; +} + +.comment-msgs { + display: flex; + justify-content: space-between; + gap: 5px; +} + +.comment-msg { + font-style: italic; + text-align: start; + padding-bottom: 5px; + font-size: 14px; +} + +.comment-select-checkbox, +.comment-select-checkbox:focus { + display: none; + appearance: none; + outline: none; + position: relative; + right: 10px; + bottom: 8px; + width: 7%; + height: 10%; + color: #FFA646; + font-size: 2em; + background-color: #FAFAFA; +} + +.comment-img { + height: 150px; + width: 150px; + object-fit: cover; +} + +.comment-author, +.comment-date { + display: flex; + align-items: center; + gap: 5px; + font-weight: 600; + color: #3C3C3C; + justify-content: end; + font-size: 12px; +} + +.comment-author-icon { + padding: 0px; + height: 16px; + width: 16px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + text-align: center; + font-size: 10px; +} + +.comment-author-icon::before { + width: 12px; + height: 16px; + place-content: center; +} + +.comment-buttons button { + border: 0; + border-radius: 6px; + padding: 5px 10px; + font-weight: bold; + font-size: 12px; + cursor: pointer; +} + +.comment-validate { + background-color: #FFC17E; + color: #3C3C3C; +} + +.comment-validate:hover { + background-color: #ff7700; + color: #493C21; +} + +.comment-delete { + background-color: #EEEEEE; + color: #3C3C3C; +} + +.comment-delete:hover { + background-color: #E3E3E3; +} + +.comments .user-pagination { + padding: 20px 20px 0 0; +} + +.comments-selected { + margin: 20px 30px; +} + +.comments-no-selection { + margin: 0 30px !important; +} + +.comments-selected-item { + display: flex; + align-items: center; + font-size: 16px; + gap: 5px; +} + +.comment-paging-current { + color: #ffa646; +} + +.comments-reset-filters { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; +} + +.comments-reset-filters:hover { + color: #232323; +} + +.comments-modal { + width: 700px !important; +} + +.comments-modal p { + margin: 0; +} + +.comments-modal-header { + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +.comments-modal-infos { + display: flex; + flex-direction: column; + gap: 5px; + align-items: start; +} + +.comments-modal-img-info { + display: flex; + align-items: start; + gap: 5px; +} + +.comments-modal-img-i { + display: flex; + flex-direction: column; + gap: 5px; + align-items: self-end; +} + +.comments-modal .comment-author { + justify-content: start; + font-size: 18px; + flex-direction: row-reverse; + font-weight: bold; +} + +.comments-modal .comment-author-icon { + font-size: 16px; + height: 24px; + width: 24px; +} + +.comments-modal .comment-author-icon::before { + width: 16px; + height: 20px; +} + +.comments-modal-img { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 2px; +} + +.comments-modal-body { + width: 100%; + max-height: 200px; + overflow-y: auto; + margin: 15px 0; } .bottomButtons.syncBtn { @@ -5121,7 +5247,7 @@ a#showPermissions:hover {text-decoration: none;} margin-left: 25px; } - +/* Category Ordering */ FORM#categoryOrdering p.albumTitle {margin:0; margin-left: 5px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; margin-bottom: 4px;} FORM#categoryOrdering p.albumTitle a {font-size: 14px; font-weight: 600;} FORM#categoryOrdering p.albumTitle i {display: none;} @@ -5859,7 +5985,8 @@ input:checked + .slider.small:before, input:checked + .slider.small::after { white-space: nowrap; } -#selection-mode-block button{ +#selection-mode-block button, +#commentsSelection button { display:block; margin:20px auto; font-size: 12px; @@ -5869,7 +5996,8 @@ input:checked + .slider.small:before, input:checked + .slider.small::after { width: 180px; } -#selection-mode-block button:hover{ +#selection-mode-block button:hover, +#commentsSelection button:hover { cursor: pointer; } @@ -6836,23 +6964,6 @@ fieldset#environment legend i[class*="icon-"] { color:#493C21; } -.buttonSecondary{ - font-size:12px; - border:none; - padding:13px 20px; - margin-left:0; font-weight: bold; transition: all 125ms ease-out; - color:#3C3C3C; - background-color:#ECECEC; -} - -.buttonSecondary:hover{ - cursor: pointer; - background-color: #FFA646; - text-decoration: none; - color:#3C3C3C; -} - - #cboxLoadedContent input[type="submit"] {margin-bottom: 20px; float: none;} #permissions, #uploadForm fieldset {border: 0;} @@ -6875,7 +6986,7 @@ fieldset#environment legend i[class*="icon-"] { .icon-plus.cboxElement:hover {box-shadow: 0 2px 4px rgba(0, 0, 0, 0.16);} #albumSelection {margin-right: 20px;} .selectize-control.single .selectize-input {border-radius: 0; font-weight: bold;font-size: 12px;} -.buttonLike, .buttonSecondary {padding: 8px 10px; margin-left: 5px;} +.buttonLike {padding: 8px 10px; margin-left: 5px;} .changeUsername .buttonLike {padding: 1px 10px;} .changePassword .buttonLike {padding: 1px 10px;} .selectFilesButtonBlock {display: flex; margin-top: 10px;} @@ -8063,6 +8174,7 @@ color:#FF7B00; font-size: 30px; } +.modal-content, .new-album-modal-content { display: flex; flex-direction: column; diff --git a/admin/themes/roma/theme.css b/admin/themes/roma/theme.css index 6ddea9e9a..68d48d4df 100644 --- a/admin/themes/roma/theme.css +++ b/admin/themes/roma/theme.css @@ -727,7 +727,8 @@ input:focus + .slider { box-shadow: 0 0 1px #FFC27F; } -#selection-mode-block{ +#selection-mode-block, +.selection-mode { background-color: #232323; border-left:1px solid #1a1a1a; } @@ -762,13 +763,15 @@ input:focus + .slider { color:#c0c0c0; } -#selection-mode-block button{ +#selection-mode-block button, +#commentsSelection button { border: 1px solid #e7e7e7; color:#c0c0c0; } -#selection-mode-block button:hover{ +#selection-mode-block button:hover, +#commentsSelection button:hover { background-color: #ffa744; border: 1px solid #ffa744; color:#3c3c3c; @@ -1301,7 +1304,8 @@ background:#6C2D2D!important; /* Tag Manager */ .tag-box, -.tag-header .selection-controller a{ +.tag-header .selection-controller a, +.comments-selection-controller .comments-selection-btn { background-color: #333 !important; } @@ -1494,6 +1498,7 @@ background:#6C2D2D!important; transform: translateY(13px); } +.tab-filters label, .pluginTypeFilter label { display: flex; flex-direction: row; @@ -1509,25 +1514,30 @@ background:#6C2D2D!important; box-shadow: 0px 2px #00000066; } +.tab-filters label:hover, .pluginTypeFilter label:hover{ background: #1B1B1B; } +.tab-filters input:checked + label, .pluginTypeFilter input:checked + label{ background: #ffa646; color: #444444; box-shadow: 0 2px #925f00; } +.tab-filters input:checked:hover + label, .pluginTypeFilter input:checked:hover + label{ background: #ffc17e; } +.tab-filters input:checked + label .filter-badge, .pluginTypeFilter input:checked + label .filter-badge { background-color: #d18800; color: #444; } +.tab-filters input + label .filter-badge, .pluginTypeFilter input + label .filter-badge { background-color: #3f3f3f; color: #bababa; @@ -2275,7 +2285,46 @@ ul.jqtree-tree li.jqtree-ghost span.jqtree-line { opacity:60%!important; } +.comment-container { + background-color: #333; + border-color: #ffa646; +} + +.comment-validated { + border-color: #232323 !important; +} + +.comment-author, +.comment-date { + color: #aaa; +} + +.comment-validate { + background-color: #ffa646; +} + +.comment-delete { + background-color: #232323; +} + +.comment-delete:hover { + background-color: #111; +} + +.comments-advanced-filters { + background-color: #333; +} + +.comment.comment-selected .comment-content { + background-color: #1B1B1B; +} + +.comments-container.active .comment-content:hover { + background-color: #1B1B1B; +} + /* Cat modify description modal */ +.modal-content, .desc-modal-content, .new-album-modal-content { background-color: #444; @@ -2394,64 +2443,6 @@ ul.jqtree-tree li.jqtree-ghost span.jqtree-line { background-color:#444; } -.comment-box, .comment-box:focus{ - background-color: #333; - color: #777; - border-color: #777; -} - -.comment-box .comment{ - color : #777; -} - -.comment-selection-content{ - background-color: rgb(51, 51, 51); - border-left: 1px solid #3A3A3A; -} - -.badge-grey{ - background-color: rgb(63, 63, 63); - color: rgb(186, 186, 186); -} - -.selectButton{ - background-color: rgb(85, 85, 85); - color: rgb(193, 193, 193); -} - -.selectButton2{ - border-color: rgb(85, 85, 85); - color: #C1C1C1; -} - -.delete-comment{ - background-color: rgb(85, 85, 85); - color: rgb(193, 193, 193); -} - -.comment-select-checkbox, .comment-select-checkbox:focus{ - background-color: #333; -} - -.commentFilterSelected, .commentFilterSelected:hover { - color: #FFFFFF; - background-color: #FFA500; - border-bottom: solid #D18800; -} -.commentFilterUnselected, .commentFilterUnselected:hover { - color: #F3F3F3; - background-color: #898989; - border-bottom: solid #DBDBDB; -} - -.buttonSecondary { - background-color: #2E2E2E; - color: #777777; -} -.buttonSecondary:hover { - background-color: #1B1B1B; - color: #777777; - /* Filters options */ .select-views{ background-color: #444444; @@ -2480,4 +2471,20 @@ ul.jqtree-tree li.jqtree-ghost span.jqtree-line { label:has(> .filters-icon-check:disabled){ color:#555; -} \ No newline at end of file +} + +.comments-advanced-filter .advanced-filter-btn { + box-shadow: 0px 2px #333; +} + +.comments-search { + background-color: #333 !important; +} + +.comments-search:hover { + background-color: #222 !important +} + +.comments-search input:focus { + outline: none; +} diff --git a/include/ws_functions/pwg.comments.php b/include/ws_functions/pwg.comments.php new file mode 100644 index 000000000..11e7c4cb1 --- /dev/null +++ b/include/ws_functions/pwg.comments.php @@ -0,0 +1,243 @@ += \''. $min .'\''; + } + + if (!empty($params['f_max_date'])) + { + $max = date_format(date_create($params['f_max_date']), "Y-m-d 23:59:59"); + $where_clauses[] = 'date <= \''. $max .'\''; + } + + // reset all filters during search + if (!empty($params['search'])) + { + $where_clauses = array('1=1'); + $where_clauses[] = 'content LIKE "%'. pwg_db_real_escape_string($params['search']) .'%"'; + } + + // summary + $query = ' +SELECT + count(*) as all_comments, + sum(validated = \'true\') as validated, + sum(validated = \'false\') as pending +FROM '.COMMENTS_TABLE.' +WHERE '.implode(' AND ', $where_clauses).' +;'; + + $summary = pwg_db_fetch_assoc(pwg_query($query)); + $total_comments = $summary['all_comments']; + + switch($params['status']) + { + case 'pending': + $where_clauses[] = 'validated = \'false\''; + $total_comments = $summary['pending']; + break; + + case 'validated': + $where_clauses[] = 'validated = \'true\''; + $total_comments = $summary['validated']; + break; + } + + // comments + $query = ' +SELECT + c.id, + c.image_id, + c.date, + c.author, + c.author_id, + '.$conf['user_fields']['username'].' AS username, + ui.status, + c.content, + i.path, + i.representative_ext, + i.file, + i.date_available, + validated, + c.anonymous_id + FROM '.COMMENTS_TABLE.' AS c + INNER JOIN '.IMAGES_TABLE.' AS i + ON i.id = c.image_id + LEFT JOIN '.USERS_TABLE.' AS u + ON u.'.$conf['user_fields']['id'].' = c.author_id + LEFT JOIN '.USER_INFOS_TABLE.' AS ui + ON ui.user_id = c.author_id + WHERE '.implode(' AND ', $where_clauses).' + ORDER BY c.date DESC + LIMIT '.$params['per_page'] * $params['page'].', '.$params['per_page'].' +;'; + $result = pwg_query($query); + + $list = array(); + while ($row = pwg_db_fetch_assoc($result)) + { + + $medium = DerivativeImage::get_one( + IMG_MEDIUM, + array( + 'id' => $row['image_id'], + 'path' => $row['path'], + 'representative_ext' => $row['representative_ext'], + ) + )->get_url(); + + if (empty($row['author_id'])) + { + $author_name = $row['author']; + } + else + { + $author_name = stripslashes($row['username']); + } + + $list[] = array( + 'id' => $row['id'], + 'admin_link' => get_root_url().'admin.php?page=photo-'.$row['image_id'], + 'medium_url' => $medium, + 'file' => $row['file'], + 'image_date_available' => format_date($row['date_available'], array('day_name','day','month','year','time')), + 'author' => trigger_change('render_comment_author', $author_name), + 'author_status' => $conf['webmaster_id'] == $row['author_id'] ? 'main_user' : $row['status'], + 'date' => format_date($row['date'], array('day_name','day','month','year','time')), + 'content' => trigger_change('render_comment_content', $row['content']), + 'raw_content' => $row['content'], + 'is_pending' => ('false' == $row['validated']), + ); + } + + // filters + $query = ' +SELECT + MIN(date) AS started_at, + MAX(date) AS ended_at +FROM '.COMMENTS_TABLE.' +WHERE '.implode(' AND ', $where_clauses).' +;'; + + $dates = pwg_db_fetch_assoc(pwg_query($query)); + + unset($where_clauses['author_id']); + $query = ' +SELECT + author, + author_id, + count(*) as nb_authors +FROM '.COMMENTS_TABLE.' +WHERE '.implode(' AND ', $where_clauses).' +GROUP BY author_id +;'; + + $nb_authors_in = query2array($query); + + return array( + 'summary' => $summary, + 'comments' => $list, + 'filters' => array( + 'nb_authors' => $nb_authors_in, + 'started_at' => $dates['started_at'], + 'ended_at' => $dates['ended_at'] + ), + 'paging' => array( + 'page' => $params['page'], + 'per_page' => $params['per_page'], + 'total_pages' => max(0, ceil($total_comments / $params['per_page']) - 1), + ), + ); +} + +/** + * API method + * Delete comments + * @since 16 + * @param mixed[] $params + * + */ +function ws_userComments_delete($params, &$service) +{ + include_once(PHPWG_ROOT_PATH.'include/functions_comment.inc.php'); + + if (get_pwg_token() != $params['pwg_token']) + { + return new PwgError(403, l10n('Invalid security token')); + } + + $params['comment_id'] = array_unique($params['comment_id']); + delete_user_comment($params['comment_id']); + return 'Comment successfully deleted'; +} + +/** + * API method + * Validate comments + * @since 16 + * @param mixed[] $params + * + */ +function ws_userComments_validate($params, &$service) +{ + include_once(PHPWG_ROOT_PATH.'include/functions_comment.inc.php'); + + if (get_pwg_token() != $params['pwg_token']) + { + return new PwgError(403, l10n('Invalid security token')); + } + + $params['comment_id'] = array_unique($params['comment_id']); + validate_user_comment($params['comment_id']); + return 'Comment successfully validated'; +} \ No newline at end of file diff --git a/language/en_UK/admin.lang.php b/language/en_UK/admin.lang.php index 5ab96daf5..a64bccd4d 100644 --- a/language/en_UK/admin.lang.php +++ b/language/en_UK/admin.lang.php @@ -1411,5 +1411,8 @@ $lang['Save all photos'] = 'Save all photos'; $lang['Use standard Piwigo template for common pages.'] = 'Use standard Piwigo template for common pages.'; $lang['When enabled, a common template is used for the login, registration and forgotten password pages, regardless of the theme. Some themes might use these templates even if you uncheck this option'] = 'When enabled, a common template is used for the login, registration and forgotten password pages, regardless of the theme. Some themes might use these templates even if you uncheck this option'; $lang['We have found %d duplicate paths. Details provided by plugin Check Uploads'] = 'We have found %d duplicate paths. Details provided by plugin Check Uploads'; +$lang['Are you sure you want to delete comment "%s"?'] = 'Are you sure you want to delete comment "%s"?'; +$lang['No comments selected, no actions possible.'] = 'No comments selected, no actions possible.'; +$lang['The comment has been validated.'] = 'The comment has been validated.'; // Leave this line empty \ No newline at end of file diff --git a/language/fr_FR/admin.lang.php b/language/fr_FR/admin.lang.php index 2b0d5b94e..998029de6 100644 --- a/language/fr_FR/admin.lang.php +++ b/language/fr_FR/admin.lang.php @@ -1413,4 +1413,7 @@ $lang['Save all photos'] = 'Enregistrer toutes les photos'; $lang['Use standard Piwigo template for common pages.'] = 'Utiliser le modèle standard de Piwigo pour les pages courantes.'; $lang['When enabled, a common template is used for the login, registration and forgotten password pages, regardless of the theme. Some themes might use these templates even if you uncheck this option'] = 'Lorsque cette option est activée, un modèle commun est utilisé pour les pages de connexion, d\'inscription et de mot de passe oublié, quel que soit le thème. Certains thèmes peuvent utiliser ces modèles même si cette option est décochée.'; $lang['We have found %d duplicate paths. Details provided by plugin Check Uploads'] = 'Nous avons trouvé %d chemins anormalement dupliqués. À contrôler avec le plugin Check Uploads.'; +$lang['Are you sure you want to delete comment "%s"?'] = 'Êtes-vous sûr de vouloir supprimer le commentaire "%s" ?'; +$lang['No comments selected, no actions possible.'] = 'Aucun commentaire sélectionné, aucune action possible.'; +$lang['The comment has been validated.'] = 'Le commentaire a été validé.'; // Leave this line empty diff --git a/ws.php b/ws.php index 484e801c5..a8628418b 100644 --- a/ws.php +++ b/ws.php @@ -1623,6 +1623,85 @@ enabled_high, registration_date, registration_date_string, registration_date_sin $ws_functions_root . 'pwg.users.php', array('admin_only'=>false, 'post_only'=>true) ); + + $service->addMethod( + 'pwg.userComments.getList', + 'ws_userComments_getList', + array( + 'status' => array( + 'default' => 'all', + 'info' => 'must be: all, validated or pending' + ), + 'search' => array( + 'default' => null, + 'info' => 'All other parameters are not used during a search.' + ), + 'author_id' => array( + 'flags' => WS_PARAM_OPTIONAL, + 'type' => WS_TYPE_ID, + ), + 'image_id' => array( + 'flags' => WS_PARAM_OPTIONAL, + 'type' => WS_TYPE_ID, + ), + 'f_min_date' => array( + 'default' => null + ), + 'f_max_date' => array( + 'default' => null + ), + 'page' => array( + 'default' => 0, + 'type' => WS_TYPE_INT | WS_TYPE_POSITIVE + ), + 'per_page' => array( + 'default' => $conf['comments_page_nb_comments'], + 'type' => WS_TYPE_INT | WS_TYPE_POSITIVE + ) + ), + 'Get comments', + $ws_functions_root . 'pwg.comments.php', + array( + 'admin_only' => true, + 'post_only' => false + ) + ); + + $service->addMethod( + 'pwg.userComments.delete', + 'ws_userComments_delete', + array( + 'comment_id' => array( + 'flags' => WS_PARAM_FORCE_ARRAY, + 'type' => WS_TYPE_INT | WS_TYPE_POSITIVE, + ), + 'pwg_token' => array(), + ), + 'Delete comments', + $ws_functions_root . 'pwg.comments.php', + array( + 'admin_only'=>true, + 'post_only'=>true + ) + ); + + $service->addMethod( + 'pwg.userComments.validate', + 'ws_userComments_validate', + array( + 'comment_id' => array( + 'flags' => WS_PARAM_FORCE_ARRAY, + 'type' => WS_TYPE_INT | WS_TYPE_POSITIVE, + ), + 'pwg_token' => array(), + ), + 'Validate comments', + $ws_functions_root . 'pwg.comments.php', + array( + 'admin_only'=>true, + 'post_only'=>true + ) + ); } ?> From 613dd410e53097ea0e6bf87b11f75dcb533dfda0 Mon Sep 17 00:00:00 2001 From: Linty Date: Tue, 26 Aug 2025 16:38:11 +0200 Subject: [PATCH 023/149] issue #2364 fix guest author name and language key Added support for bulk comment validation and deletion with updated confirmation messages and translations. Improved modal dialog behavior, including keyboard accessibility and button visibility. Updated CSS for consistent button styling across themes. Fixed author name display for guest comments. --- admin/themes/clear/theme.css | 8 ++++- admin/themes/default/js/comments.js | 36 +++++++++++++++++++--- admin/themes/default/template/comments.tpl | 7 +++-- admin/themes/default/theme.css | 7 ++++- admin/themes/roma/theme.css | 10 ++++++ include/ws_functions/pwg.comments.php | 2 +- language/en_UK/admin.lang.php | 4 ++- language/fr_FR/admin.lang.php | 4 ++- 8 files changed, 67 insertions(+), 11 deletions(-) diff --git a/admin/themes/clear/theme.css b/admin/themes/clear/theme.css index e7a4f494a..e7ad821c4 100644 --- a/admin/themes/clear/theme.css +++ b/admin/themes/clear/theme.css @@ -247,11 +247,17 @@ UL.thumbnails li.rank-of-image {background-color: #ddd;} font-weight: bold; } -#batchManagerGlobal #applyFilter,#batchManagerGlobal .addFilter-button { +#batchManagerGlobal #applyFilter,#batchManagerGlobal .addFilter-button, +.buttonSecondary { color: #3C3C3C; background-color: #ececec; } +.buttonSecondary:hover { + color: #3C3C3C; + background-color: #aaa; +} + #batchManagerGlobal #applyFilter:hover, #batchManagerGlobal .addFilter-button:hover { color: #111; background-color: #ff9b32 ; diff --git a/admin/themes/default/js/comments.js b/admin/themes/default/js/comments.js index f7254c1bb..ca4844ffc 100644 --- a/admin/themes/default/js/comments.js +++ b/admin/themes/default/js/comments.js @@ -110,6 +110,12 @@ $(function() { commentsClearFilters(); }); + $(window).on('keydown', function(e) { + if (e.key === 'Escape') { + closeModalViewComment(); + } + }); + // get comments and set display commentsParams.per_page = window.localStorage.getItem('adminCommentsNB') ?? 5 updateNbComments(commentsParams.per_page); @@ -320,8 +326,8 @@ function commentsDisplayFilters(filters) { // reset here to let decide filterAuthor onChange updateAuthorId = true; - const minDate = filters.started_at.split(' ')[0] ?? ''; - const maxDate = filters.ended_at.split(' ')[0] ?? '' + const minDate = filters.started_at?.split(' ')[0] ?? ''; + const maxDate = filters.ended_at?.split(' ')[0] ?? '' filterDateStart.val(minDate).attr({ 'min': minDate, 'max': maxDate }); filterDateEnd.val(maxDate).attr({ 'max': maxDate, 'min': minDate }); @@ -404,14 +410,34 @@ function showModalViewComment(id) { `); modalViewComment.find('.comments-modal-body').html(comment.content) + const validBtn = modalViewComment.find('.comments-modal-validate'); + if (comment.is_pending) { + validBtn.show(); + $('#commentsModalValidate').off('click').on('click', function() { + validateComment([id]); + closeModalViewComment(); + }); + } else { + validBtn.hide(); + } + + $('#commentsModalDelete').off('click').on('click', function() { + deleteComment([id]); + closeModalViewComment(); + }); + modalViewComment.fadeIn(); } function closeModalViewComment() { modalViewComment.fadeOut(); + $('#commentsModalValidate').off('click'); + $('#commentsModalDelete').off('click') } function validateComment(id) { + const idLenght = id.length ?? 1; + $.ajax({ url: 'ws.php?format=json&method=pwg.userComments.validate', type: 'POST', @@ -424,7 +450,7 @@ function validateComment(id) { if (res.stat === 'ok') { $.alert({ ...{ - title: str_comment_validated, + title: idLenght > 1 ? str_comments_validated : str_comment_validated, content: "", }, ...jConfirm_alert_options @@ -454,8 +480,10 @@ function validateComment(id) { } function deleteComment(id) { + const idLenght = id.length ?? 1; + $.confirm({ - title: str_delete.replace("%s", id), + title: idLenght > 1 ? str_deletes.replace("%d", idLenght) : str_delete.replace("%s", id), draggable: false, titleClass: "jconfirmDeleteConfirm", theme: "modern", diff --git a/admin/themes/default/template/comments.tpl b/admin/themes/default/template/comments.tpl index de680e204..7070b31e6 100644 --- a/admin/themes/default/template/comments.tpl +++ b/admin/themes/default/template/comments.tpl @@ -5,11 +5,13 @@ {footer_script} const str_yes_delete_confirmation = "{'Yes, delete'|@translate|@escape:'javascript'}" const str_no_delete_confirmation = "{"No, I have changed my mind"|@translate|@escape:'javascript'}" -const str_delete = "{'Are you sure you want to delete comment "%s"?'|@translate|@escape:'javascript'}" +const str_delete = "{'Are you sure you want to delete comment #%s?'|@translate|@escape:'javascript'}" +const str_deletes = "{'Are you sure you want to delete "%d" comments?'|@translate|@escape:'javascript'}" const str_no_comments_selected = "{'No comments selected, no actions possible.'|@translate|@escape:'javascript'}" const pwg_token = "{$PWG_TOKEN}" const str_an_error_has = "{"An error has occured"|@translate|@escape:'javascript'}" const str_comment_validated = "{"The comment has been validated."|@translate|@escape:'javascript'}" +const str_comments_validated = "{"The comments have been validated."|@translate|@escape:'javascript'}" const str_and_others = "{"and %s others"|@translate}" {/footer_script} +


        '; + echo join("
        ", $frame); + echo '






'; + + } else { + + foreach ($frame as &$frameLine) { + $frameLine = join(' ', explode("\xc0", $frameLine)); + $frameLine = join('', explode("\xc1", $frameLine)); + $frameLine = join(' ', explode("\xa0", $frameLine)); + $frameLine = join('', explode("\xa1", $frameLine)); + $frameLine = join('', explode("\x84", $frameLine)); //format 0 + $frameLine = join('', explode("\x85", $frameLine)); //format 1 + $frameLine = join('', explode("\x81", $frameLine)); //special bit + $frameLine = join(' ', explode("\x90", $frameLine)); //clock 0 + $frameLine = join('', explode("\x91", $frameLine)); //clock 1 + $frameLine = join(' ', explode("\x88", $frameLine)); //version + $frameLine = join('', explode("\x89", $frameLine)); //version + $frameLine = join('♦', explode("\x01", $frameLine)); + $frameLine = join('⋅', explode("\0", $frameLine)); + } + + ?> + + "; + echo join("
", $frame); + echo "
"; + + } + } + + //---------------------------------------------------------------------- + public static function serial($frame) + { + return gzcompress(join("\n", $frame), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + return explode("\n", gzuncompress($code)); + } + + //---------------------------------------------------------------------- + public static function newFrame($version) + { + if($version < 1 || $version > QRSPEC_VERSION_MAX) + return null; + + if(!isset(self::$frames[$version])) { + + $fileName = QR_CACHE_DIR.'frame_'.$version.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + self::$frames[$version] = self::unserial(file_get_contents($fileName)); + } else { + self::$frames[$version] = self::createFrame($version); + file_put_contents($fileName, self::serial(self::$frames[$version])); + } + } else { + self::$frames[$version] = self::createFrame($version); + } + } + + if(is_null(self::$frames[$version])) + return null; + + return self::$frames[$version]; + } + + //---------------------------------------------------------------------- + public static function rsBlockNum($spec) { return $spec[0] + $spec[3]; } + public static function rsBlockNum1($spec) { return $spec[0]; } + public static function rsDataCodes1($spec) { return $spec[1]; } + public static function rsEccCodes1($spec) { return $spec[2]; } + public static function rsBlockNum2($spec) { return $spec[3]; } + public static function rsDataCodes2($spec) { return $spec[4]; } + public static function rsEccCodes2($spec) { return $spec[2]; } + public static function rsDataLength($spec) { return ($spec[0] * $spec[1]) + ($spec[3] * $spec[4]); } + public static function rsEccLength($spec) { return ($spec[0] + $spec[3]) * $spec[2]; } + + } + + + +//---- qrimage.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Image output of code using GD2 + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('QR_IMAGE', true); + + class QRimage { + + //---------------------------------------------------------------------- + public static function png($frame, $filename = false, $pixelPerPoint = 4, $outerFrame = 4,$saveandprint=FALSE) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/png"); + ImagePng($image); + } else { + if($saveandprint===TRUE){ + ImagePng($image, $filename); + header("Content-type: image/png"); + ImagePng($image); + }else{ + ImagePng($image, $filename); + } + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + public static function jpg($frame, $filename = false, $pixelPerPoint = 8, $outerFrame = 4, $q = 85) + { + $image = self::image($frame, $pixelPerPoint, $outerFrame); + + if ($filename === false) { + Header("Content-type: image/jpeg"); + ImageJpeg($image, null, $q); + } else { + ImageJpeg($image, $filename, $q); + } + + ImageDestroy($image); + } + + //---------------------------------------------------------------------- + private static function image($frame, $pixelPerPoint = 4, $outerFrame = 4) + { + $h = count($frame); + $w = strlen($frame[0]); + + $imgW = $w + 2*$outerFrame; + $imgH = $h + 2*$outerFrame; + + $base_image =ImageCreate($imgW, $imgH); + + $col[0] = ImageColorAllocate($base_image,255,255,255); + $col[1] = ImageColorAllocate($base_image,0,0,0); + + imagefill($base_image, 0, 0, $col[0]); + + for($y=0; $y<$h; $y++) { + for($x=0; $x<$w; $x++) { + if ($frame[$y][$x] == '1') { + ImageSetPixel($base_image,$x+$outerFrame,$y+$outerFrame,$col[1]); + } + } + } + + $target_image =ImageCreate($imgW * $pixelPerPoint, $imgH * $pixelPerPoint); + ImageCopyResized($target_image, $base_image, 0, 0, 0, 0, $imgW * $pixelPerPoint, $imgH * $pixelPerPoint, $imgW, $imgH); + ImageDestroy($base_image); + + return $target_image; + } + } + + + +//---- qrinput.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Input encoding class + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('STRUCTURE_HEADER_BITS', 20); + define('MAX_STRUCTURED_SYMBOLS', 16); + + class QRinputItem { + + public $mode; + public $size; + public $data; + public $bstream; + + public function __construct($mode, $size, $data, $bstream = null) + { + $setData = array_slice($data, 0, $size); + + if (count($setData) < $size) { + $setData = array_merge($setData, array_fill(0,$size-count($setData),0)); + } + + if(!QRinput::check($mode, $size, $setData)) { + throw new Exception('Error m:'.$mode.',s:'.$size.',d:'.join(',',$setData)); + return null; + } + + $this->mode = $mode; + $this->size = $size; + $this->data = $setData; + $this->bstream = $bstream; + } + + //---------------------------------------------------------------------- + public function encodeModeNum($version) + { + try { + + $words = (int)($this->size / 3); + $bs = new QRbitstream(); + + $val = 0x1; + $bs->appendNum(4, $val); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_NUM, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (ord($this->data[$i*3 ]) - ord('0')) * 100; + $val += (ord($this->data[$i*3+1]) - ord('0')) * 10; + $val += (ord($this->data[$i*3+2]) - ord('0')); + $bs->appendNum(10, $val); + } + + if($this->size - $words * 3 == 1) { + $val = ord($this->data[$words*3]) - ord('0'); + $bs->appendNum(4, $val); + } else if($this->size - $words * 3 == 2) { + $val = (ord($this->data[$words*3 ]) - ord('0')) * 10; + $val += (ord($this->data[$words*3+1]) - ord('0')); + $bs->appendNum(7, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeAn($version) + { + try { + $words = (int)($this->size / 2); + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x02); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_AN, $version), $this->size); + + for($i=0; $i<$words; $i++) { + $val = (int)QRinput::lookAnTable(ord($this->data[$i*2 ])) * 45; + $val += (int)QRinput::lookAnTable(ord($this->data[$i*2+1])); + + $bs->appendNum(11, $val); + } + + if($this->size & 1) { + $val = QRinput::lookAnTable(ord($this->data[$words * 2])); + $bs->appendNum(6, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeMode8($version) + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x4); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_8, $version), $this->size); + + for($i=0; $i<$this->size; $i++) { + $bs->appendNum(8, ord($this->data[$i])); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeKanji($version) + { + try { + + $bs = new QRbitrtream(); + + $bs->appendNum(4, 0x8); + $bs->appendNum(QRspec::lengthIndicator(QR_MODE_KANJI, $version), (int)($this->size / 2)); + + for($i=0; $i<$this->size; $i+=2) { + $val = (ord($this->data[$i]) << 8) | ord($this->data[$i+1]); + if($val <= 0x9ffc) { + $val -= 0x8140; + } else { + $val -= 0xc140; + } + + $h = ($val >> 8) * 0xc0; + $val = ($val & 0xff) + $h; + + $bs->appendNum(13, $val); + } + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function encodeModeStructure() + { + try { + $bs = new QRbitstream(); + + $bs->appendNum(4, 0x03); + $bs->appendNum(4, ord($this->data[1]) - 1); + $bs->appendNum(4, ord($this->data[0]) - 1); + $bs->appendNum(8, ord($this->data[2])); + + $this->bstream = $bs; + return 0; + + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function estimateBitStreamSizeOfEntry($version) + { + $bits = 0; + + if($version == 0) + $version = 1; + + switch($this->mode) { + case QR_MODE_NUM: $bits = QRinput::estimateBitsModeNum($this->size); break; + case QR_MODE_AN: $bits = QRinput::estimateBitsModeAn($this->size); break; + case QR_MODE_8: $bits = QRinput::estimateBitsMode8($this->size); break; + case QR_MODE_KANJI: $bits = QRinput::estimateBitsModeKanji($this->size);break; + case QR_MODE_STRUCTURE: return STRUCTURE_HEADER_BITS; + default: + return 0; + } + + $l = QRspec::lengthIndicator($this->mode, $version); + $m = 1 << $l; + $num = (int)(($this->size + $m - 1) / $m); + + $bits += $num * (4 + $l); + + return $bits; + } + + //---------------------------------------------------------------------- + public function encodeBitStream($version) + { + try { + + unset($this->bstream); + $words = QRspec::maximumWords($this->mode, $version); + + if($this->size > $words) { + + $st1 = new QRinputItem($this->mode, $words, $this->data); + $st2 = new QRinputItem($this->mode, $this->size - $words, array_slice($this->data, $words)); + + $st1->encodeBitStream($version); + $st2->encodeBitStream($version); + + $this->bstream = new QRbitstream(); + $this->bstream->append($st1->bstream); + $this->bstream->append($st2->bstream); + + unset($st1); + unset($st2); + + } else { + + $ret = 0; + + switch($this->mode) { + case QR_MODE_NUM: $ret = $this->encodeModeNum($version); break; + case QR_MODE_AN: $ret = $this->encodeModeAn($version); break; + case QR_MODE_8: $ret = $this->encodeMode8($version); break; + case QR_MODE_KANJI: $ret = $this->encodeModeKanji($version);break; + case QR_MODE_STRUCTURE: $ret = $this->encodeModeStructure(); break; + + default: + break; + } + + if($ret < 0) + return -1; + } + + return $this->bstream->size(); + + } catch (Exception $e) { + return -1; + } + } + }; + + //########################################################################## + + class QRinput { + + public $items; + + private $version; + private $level; + + //---------------------------------------------------------------------- + public function __construct($version = 0, $level = QR_ECLEVEL_L) + { + if ($version < 0 || $version > QRSPEC_VERSION_MAX || $level > QR_ECLEVEL_H) { + throw new Exception('Invalid version no'); + return NULL; + } + + $this->version = $version; + $this->level = $level; + } + + //---------------------------------------------------------------------- + public function getVersion() + { + return $this->version; + } + + //---------------------------------------------------------------------- + public function setVersion($version) + { + if($version < 0 || $version > QRSPEC_VERSION_MAX) { + throw new Exception('Invalid version no'); + return -1; + } + + $this->version = $version; + + return 0; + } + + //---------------------------------------------------------------------- + public function getErrorCorrectionLevel() + { + return $this->level; + } + + //---------------------------------------------------------------------- + public function setErrorCorrectionLevel($level) + { + if($level > QR_ECLEVEL_H) { + throw new Exception('Invalid ECLEVEL'); + return -1; + } + + $this->level = $level; + + return 0; + } + + //---------------------------------------------------------------------- + public function appendEntry(QRinputItem $entry) + { + $this->items[] = $entry; + } + + //---------------------------------------------------------------------- + public function append($mode, $size, $data) + { + try { + $entry = new QRinputItem($mode, $size, $data); + $this->items[] = $entry; + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + + public function insertStructuredAppendHeader($size, $index, $parity) + { + if( $size > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong size'); + } + + if( $index <= 0 || $index > MAX_STRUCTURED_SYMBOLS ) { + throw new Exception('insertStructuredAppendHeader wrong index'); + } + + $buf = array($size, $index, $parity); + + try { + $entry = new QRinputItem(QR_MODE_STRUCTURE, 3, buf); + array_unshift($this->items, $entry); + return 0; + } catch (Exception $e) { + return -1; + } + } + + //---------------------------------------------------------------------- + public function calcParity() + { + $parity = 0; + + foreach($this->items as $item) { + if($item->mode != QR_MODE_STRUCTURE) { + for($i=$item->size-1; $i>=0; $i--) { + $parity ^= $item->data[$i]; + } + } + } + + return $parity; + } + + //---------------------------------------------------------------------- + public static function checkModeNum($size, $data) + { + for($i=0; $i<$size; $i++) { + if((ord($data[$i]) < ord('0')) || (ord($data[$i]) > ord('9'))){ + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeNum($size) + { + $w = (int)$size / 3; + $bits = $w * 10; + + switch($size - $w * 3) { + case 1: + $bits += 4; + break; + case 2: + $bits += 7; + break; + default: + break; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static $anTable = array( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + ); + + //---------------------------------------------------------------------- + public static function lookAnTable($c) + { + return (($c > 127)?-1:self::$anTable[$c]); + } + + //---------------------------------------------------------------------- + public static function checkModeAn($size, $data) + { + for($i=0; $i<$size; $i++) { + if (self::lookAnTable(ord($data[$i])) == -1) { + return false; + } + } + + return true; + } + + //---------------------------------------------------------------------- + public static function estimateBitsModeAn($size) + { + $w = (int)($size / 2); + $bits = $w * 11; + + if($size & 1) { + $bits += 6; + } + + return $bits; + } + + //---------------------------------------------------------------------- + public static function estimateBitsMode8($size) + { + return $size * 8; + } + + //---------------------------------------------------------------------- + public function estimateBitsModeKanji($size) + { + return (int)(($size / 2) * 13); + } + + //---------------------------------------------------------------------- + public static function checkModeKanji($size, $data) + { + if($size & 1) + return false; + + for($i=0; $i<$size; $i+=2) { + $val = (ord($data[$i]) << 8) | ord($data[$i+1]); + if( $val < 0x8140 + || ($val > 0x9ffc && $val < 0xe040) + || $val > 0xebbf) { + return false; + } + } + + return true; + } + + /*********************************************************************** + * Validation + **********************************************************************/ + + public static function check($mode, $size, $data) + { + if($size <= 0) + return false; + + switch($mode) { + case QR_MODE_NUM: return self::checkModeNum($size, $data); break; + case QR_MODE_AN: return self::checkModeAn($size, $data); break; + case QR_MODE_KANJI: return self::checkModeKanji($size, $data); break; + case QR_MODE_8: return true; break; + case QR_MODE_STRUCTURE: return true; break; + + default: + break; + } + + return false; + } + + + //---------------------------------------------------------------------- + public function estimateBitStreamSize($version) + { + $bits = 0; + + foreach($this->items as $item) { + $bits += $item->estimateBitStreamSizeOfEntry($version); + } + + return $bits; + } + + //---------------------------------------------------------------------- + public function estimateVersion() + { + $version = 0; + $prev = 0; + do { + $prev = $version; + $bits = $this->estimateBitStreamSize($prev); + $version = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if ($version < 0) { + return -1; + } + } while ($version > $prev); + + return $version; + } + + //---------------------------------------------------------------------- + public static function lengthOfCode($mode, $version, $bits) + { + $payload = $bits - 4 - QRspec::lengthIndicator($mode, $version); + switch($mode) { + case QR_MODE_NUM: + $chunks = (int)($payload / 10); + $remain = $payload - $chunks * 10; + $size = $chunks * 3; + if($remain >= 7) { + $size += 2; + } else if($remain >= 4) { + $size += 1; + } + break; + case QR_MODE_AN: + $chunks = (int)($payload / 11); + $remain = $payload - $chunks * 11; + $size = $chunks * 2; + if($remain >= 6) + $size++; + break; + case QR_MODE_8: + $size = (int)($payload / 8); + break; + case QR_MODE_KANJI: + $size = (int)(($payload / 13) * 2); + break; + case QR_MODE_STRUCTURE: + $size = (int)($payload / 8); + break; + default: + $size = 0; + break; + } + + $maxsize = QRspec::maximumWords($mode, $version); + if($size < 0) $size = 0; + if($size > $maxsize) $size = $maxsize; + + return $size; + } + + //---------------------------------------------------------------------- + public function createBitStream() + { + $total = 0; + + foreach($this->items as $item) { + $bits = $item->encodeBitStream($this->version); + + if($bits < 0) + return -1; + + $total += $bits; + } + + return $total; + } + + //---------------------------------------------------------------------- + public function convertData() + { + $ver = $this->estimateVersion(); + if($ver > $this->getVersion()) { + $this->setVersion($ver); + } + + for(;;) { + $bits = $this->createBitStream(); + + if($bits < 0) + return -1; + + $ver = QRspec::getMinimumVersion((int)(($bits + 7) / 8), $this->level); + if($ver < 0) { + throw new Exception('WRONG VERSION'); + return -1; + } else if($ver > $this->getVersion()) { + $this->setVersion($ver); + } else { + break; + } + } + + return 0; + } + + //---------------------------------------------------------------------- + public function appendPaddingBit(&$bstream) + { + $bits = $bstream->size(); + $maxwords = QRspec::getDataLength($this->version, $this->level); + $maxbits = $maxwords * 8; + + if ($maxbits == $bits) { + return 0; + } + + if ($maxbits - $bits < 5) { + return $bstream->appendNum($maxbits - $bits, 0); + } + + $bits += 4; + $words = (int)(($bits + 7) / 8); + + $padding = new QRbitstream(); + $ret = $padding->appendNum($words * 8 - $bits + 4, 0); + + if($ret < 0) + return $ret; + + $padlen = $maxwords - $words; + + if($padlen > 0) { + + $padbuf = array(); + for($i=0; $i<$padlen; $i++) { + $padbuf[$i] = ($i&1)?0x11:0xec; + } + + $ret = $padding->appendBytes($padlen, $padbuf); + + if($ret < 0) + return $ret; + + } + + $ret = $bstream->append($padding); + + return $ret; + } + + //---------------------------------------------------------------------- + public function mergeBitStream() + { + if($this->convertData() < 0) { + return null; + } + + $bstream = new QRbitstream(); + + foreach($this->items as $item) { + $ret = $bstream->append($item->bstream); + if($ret < 0) { + return null; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getBitStream() + { + + $bstream = $this->mergeBitStream(); + + if($bstream == null) { + return null; + } + + $ret = $this->appendPaddingBit($bstream); + if($ret < 0) { + return null; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function getByteStream() + { + $bstream = $this->getBitStream(); + if($bstream == null) { + return null; + } + + return $bstream->toByte(); + } + } + + + + + + +//---- qrbitstream.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Bitstream class + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRbitstream { + + public $data = array(); + + //---------------------------------------------------------------------- + public function size() + { + return count($this->data); + } + + //---------------------------------------------------------------------- + public function allocate($setLength) + { + $this->data = array_fill(0, $setLength, 0); + return 0; + } + + //---------------------------------------------------------------------- + public static function newFromNum($bits, $num) + { + $bstream = new QRbitstream(); + $bstream->allocate($bits); + + $mask = 1 << ($bits - 1); + for($i=0; $i<$bits; $i++) { + if($num & $mask) { + $bstream->data[$i] = 1; + } else { + $bstream->data[$i] = 0; + } + $mask = $mask >> 1; + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public static function newFromBytes($size, $data) + { + $bstream = new QRbitstream(); + $bstream->allocate($size * 8); + $p=0; + + for($i=0; $i<$size; $i++) { + $mask = 0x80; + for($j=0; $j<8; $j++) { + if($data[$i] & $mask) { + $bstream->data[$p] = 1; + } else { + $bstream->data[$p] = 0; + } + $p++; + $mask = $mask >> 1; + } + } + + return $bstream; + } + + //---------------------------------------------------------------------- + public function append(QRbitstream $arg) + { + if (is_null($arg)) { + return -1; + } + + if($arg->size() == 0) { + return 0; + } + + if($this->size() == 0) { + $this->data = $arg->data; + return 0; + } + + $this->data = array_values(array_merge($this->data, $arg->data)); + + return 0; + } + + //---------------------------------------------------------------------- + public function appendNum($bits, $num) + { + if ($bits == 0) + return 0; + + $b = QRbitstream::newFromNum($bits, $num); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function appendBytes($size, $data) + { + if ($size == 0) + return 0; + + $b = QRbitstream::newFromBytes($size, $data); + + if(is_null($b)) + return -1; + + $ret = $this->append($b); + unset($b); + + return $ret; + } + + //---------------------------------------------------------------------- + public function toByte() + { + + $size = $this->size(); + + if($size == 0) { + return array(); + } + + $data = array_fill(0, (int)(($size + 7) / 8), 0); + $bytes = (int)($size / 8); + + $p = 0; + + for($i=0; $i<$bytes; $i++) { + $v = 0; + for($j=0; $j<8; $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$i] = $v; + } + + if($size & 7) { + $v = 0; + for($j=0; $j<($size & 7); $j++) { + $v = $v << 1; + $v |= $this->data[$p]; + $p++; + } + $data[$bytes] = $v; + } + + return $data; + } + + } + + + + +//---- qrsplit.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Input splitting classes + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * The following data / specifications are taken from + * "Two dimensional symbol -- QR-code -- Basic Specification" (JIS X0510:2004) + * or + * "Automatic identification and data capture techniques -- + * QR Code 2005 bar code symbology specification" (ISO/IEC 18004:2006) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + class QRsplit { + + public $dataStr = ''; + public $input; + public $modeHint; + + //---------------------------------------------------------------------- + public function __construct($dataStr, $input, $modeHint) + { + $this->dataStr = $dataStr; + $this->input = $input; + $this->modeHint = $modeHint; + } + + //---------------------------------------------------------------------- + public static function isdigitat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return ((ord($str[$pos]) >= ord('0'))&&(ord($str[$pos]) <= ord('9'))); + } + + //---------------------------------------------------------------------- + public static function isalnumat($str, $pos) + { + if ($pos >= strlen($str)) + return false; + + return (QRinput::lookAnTable(ord($str[$pos])) >= 0); + } + + //---------------------------------------------------------------------- + public function identifyMode($pos) + { + if ($pos >= strlen($this->dataStr)) + return QR_MODE_NUL; + + $c = $this->dataStr[$pos]; + + if(self::isdigitat($this->dataStr, $pos)) { + return QR_MODE_NUM; + } else if(self::isalnumat($this->dataStr, $pos)) { + return QR_MODE_AN; + } else if($this->modeHint == QR_MODE_KANJI) { + + if ($pos+1 < strlen($this->dataStr)) + { + $d = $this->dataStr[$pos+1]; + $word = (ord($c) << 8) | ord($d); + if(($word >= 0x8140 && $word <= 0x9ffc) || ($word >= 0xe040 && $word <= 0xebbf)) { + return QR_MODE_KANJI; + } + } + } + + return QR_MODE_8; + } + + //---------------------------------------------------------------------- + public function eatNum() + { + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + while(self::isdigitat($this->dataStr, $p)) { + $p++; + } + + $run = $p; + $mode = $this->identifyMode($p); + + if($mode == QR_MODE_8) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + if($mode == QR_MODE_AN) { + $dif = QRinput::estimateBitsModeNum($run) + 4 + $ln + + QRinput::estimateBitsModeAn(1) // + 4 + la + - QRinput::estimateBitsModeAn($run + 1);// - 4 - la + if($dif > 0) { + return $this->eatAn(); + } + } + + $ret = $this->input->append(QR_MODE_NUM, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatAn() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 0; + + while(self::isalnumat($this->dataStr, $p)) { + if(self::isdigitat($this->dataStr, $p)) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + + $dif = QRinput::estimateBitsModeAn($p) // + 4 + la + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsModeAn($q); // - 4 - la + + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + + if(!self::isalnumat($this->dataStr, $p)) { + $dif = QRinput::estimateBitsModeAn($run) + 4 + $la + + QRinput::estimateBitsMode8(1) // + 4 + l8 + - QRinput::estimateBitsMode8($run + 1); // - 4 - l8 + if($dif > 0) { + return $this->eat8(); + } + } + + $ret = $this->input->append(QR_MODE_AN, $run, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eatKanji() + { + $p = 0; + + while($this->identifyMode($p) == QR_MODE_KANJI) { + $p += 2; + } + + $ret = $this->input->append(QR_MODE_KANJI, $p, str_split($this->dataStr)); + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function eat8() + { + $la = QRspec::lengthIndicator(QR_MODE_AN, $this->input->getVersion()); + $ln = QRspec::lengthIndicator(QR_MODE_NUM, $this->input->getVersion()); + + $p = 1; + $dataStrLen = strlen($this->dataStr); + + while($p < $dataStrLen) { + + $mode = $this->identifyMode($p); + if($mode == QR_MODE_KANJI) { + break; + } + if($mode == QR_MODE_NUM) { + $q = $p; + while(self::isdigitat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeNum($q - $p) + 4 + $ln + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else if($mode == QR_MODE_AN) { + $q = $p; + while(self::isalnumat($this->dataStr, $q)) { + $q++; + } + $dif = QRinput::estimateBitsMode8($p) // + 4 + l8 + + QRinput::estimateBitsModeAn($q - $p) + 4 + $la + - QRinput::estimateBitsMode8($q); // - 4 - l8 + if($dif < 0) { + break; + } else { + $p = $q; + } + } else { + $p++; + } + } + + $run = $p; + $ret = $this->input->append(QR_MODE_8, $run, str_split($this->dataStr)); + + if($ret < 0) + return -1; + + return $run; + } + + //---------------------------------------------------------------------- + public function splitString() + { + while (strlen($this->dataStr) > 0) + { + if($this->dataStr == '') + return 0; + + $mode = $this->identifyMode(0); + + switch ($mode) { + case QR_MODE_NUM: $length = $this->eatNum(); break; + case QR_MODE_AN: $length = $this->eatAn(); break; + case QR_MODE_KANJI: + if ($hint == QR_MODE_KANJI) + $length = $this->eatKanji(); + else $length = $this->eat8(); + break; + default: $length = $this->eat8(); break; + + } + + if($length == 0) return 0; + if($length < 0) return -1; + + $this->dataStr = substr($this->dataStr, $length); + } + } + + //---------------------------------------------------------------------- + public function toUpper() + { + $stringLen = strlen($this->dataStr); + $p = 0; + + while ($p<$stringLen) { + $mode = self::identifyMode(substr($this->dataStr, $p), $this->modeHint); + if($mode == QR_MODE_KANJI) { + $p += 2; + } else { + if (ord($this->dataStr[$p]) >= ord('a') && ord($this->dataStr[$p]) <= ord('z')) { + $this->dataStr[$p] = chr(ord($this->dataStr[$p]) - 32); + } + $p++; + } + } + + return $this->dataStr; + } + + //---------------------------------------------------------------------- + public static function splitStringToQRinput($string, QRinput $input, $modeHint, $casesensitive = true) + { + if(is_null($string) || $string == '\0' || $string == '') { + throw new Exception('empty string!!!'); + } + + $split = new QRsplit($string, $input, $modeHint); + + if(!$casesensitive) + $split->toUpper(); + + return $split->splitString(); + } + } + + + +//---- qrrscode.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Reed-Solomon error correction support + * + * Copyright (C) 2002, 2003, 2004, 2006 Phil Karn, KA9Q + * (libfec is released under the GNU Lesser General Public License.) + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsItem { + + public $mm; // Bits per symbol + public $nn; // Symbols per block (= (1<= $this->nn) { + $x -= $this->nn; + $x = ($x >> $this->mm) + ($x & $this->nn); + } + + return $x; + } + + //---------------------------------------------------------------------- + public static function init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + // Common code for intializing a Reed-Solomon control block (char or int symbols) + // Copyright 2004 Phil Karn, KA9Q + // May be used under the terms of the GNU Lesser General Public License (LGPL) + + $rs = null; + + // Check parameter ranges + if($symsize < 0 || $symsize > 8) return $rs; + if($fcr < 0 || $fcr >= (1<<$symsize)) return $rs; + if($prim <= 0 || $prim >= (1<<$symsize)) return $rs; + if($nroots < 0 || $nroots >= (1<<$symsize)) return $rs; // Can't have more roots than symbol values! + if($pad < 0 || $pad >= ((1<<$symsize) -1 - $nroots)) return $rs; // Too much padding + + $rs = new QRrsItem(); + $rs->mm = $symsize; + $rs->nn = (1<<$symsize)-1; + $rs->pad = $pad; + + $rs->alpha_to = array_fill(0, $rs->nn+1, 0); + $rs->index_of = array_fill(0, $rs->nn+1, 0); + + // PHP style macro replacement ;) + $NN =& $rs->nn; + $A0 =& $NN; + + // Generate Galois field lookup tables + $rs->index_of[0] = $A0; // log(zero) = -inf + $rs->alpha_to[$A0] = 0; // alpha**-inf = 0 + $sr = 1; + + for($i=0; $i<$rs->nn; $i++) { + $rs->index_of[$sr] = $i; + $rs->alpha_to[$i] = $sr; + $sr <<= 1; + if($sr & (1<<$symsize)) { + $sr ^= $gfpoly; + } + $sr &= $rs->nn; + } + + if($sr != 1){ + // field generator polynomial is not primitive! + $rs = NULL; + return $rs; + } + + /* Form RS code generator polynomial from its roots */ + $rs->genpoly = array_fill(0, $nroots+1, 0); + + $rs->fcr = $fcr; + $rs->prim = $prim; + $rs->nroots = $nroots; + $rs->gfpoly = $gfpoly; + + /* Find prim-th root of 1, used in decoding */ + for($iprim=1;($iprim % $prim) != 0;$iprim += $rs->nn) + ; // intentional empty-body loop! + + $rs->iprim = (int)($iprim / $prim); + $rs->genpoly[0] = 1; + + for ($i = 0,$root=$fcr*$prim; $i < $nroots; $i++, $root += $prim) { + $rs->genpoly[$i+1] = 1; + + // Multiply rs->genpoly[] by @**(root + x) + for ($j = $i; $j > 0; $j--) { + if ($rs->genpoly[$j] != 0) { + $rs->genpoly[$j] = $rs->genpoly[$j-1] ^ $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[$j]] + $root)]; + } else { + $rs->genpoly[$j] = $rs->genpoly[$j-1]; + } + } + // rs->genpoly[0] can never be zero + $rs->genpoly[0] = $rs->alpha_to[$rs->modnn($rs->index_of[$rs->genpoly[0]] + $root)]; + } + + // convert rs->genpoly[] to index form for quicker encoding + for ($i = 0; $i <= $nroots; $i++) + $rs->genpoly[$i] = $rs->index_of[$rs->genpoly[$i]]; + + return $rs; + } + + //---------------------------------------------------------------------- + public function encode_rs_char($data, &$parity) + { + $MM =& $this->mm; + $NN =& $this->nn; + $ALPHA_TO =& $this->alpha_to; + $INDEX_OF =& $this->index_of; + $GENPOLY =& $this->genpoly; + $NROOTS =& $this->nroots; + $FCR =& $this->fcr; + $PRIM =& $this->prim; + $IPRIM =& $this->iprim; + $PAD =& $this->pad; + $A0 =& $NN; + + $parity = array_fill(0, $NROOTS, 0); + + for($i=0; $i< ($NN-$NROOTS-$PAD); $i++) { + + $feedback = $INDEX_OF[$data[$i] ^ $parity[0]]; + if($feedback != $A0) { + // feedback term is non-zero + + // This line is unnecessary when GENPOLY[NROOTS] is unity, as it must + // always be for the polynomials constructed by init_rs() + $feedback = $this->modnn($NN - $GENPOLY[$NROOTS] + $feedback); + + for($j=1;$j<$NROOTS;$j++) { + $parity[$j] ^= $ALPHA_TO[$this->modnn($feedback + $GENPOLY[$NROOTS-$j])]; + } + } + + // Shift + array_shift($parity); + if($feedback != $A0) { + array_push($parity, $ALPHA_TO[$this->modnn($feedback + $GENPOLY[0])]); + } else { + array_push($parity, 0); + } + } + } + } + + //########################################################################## + + class QRrs { + + public static $items = array(); + + //---------------------------------------------------------------------- + public static function init_rs($symsize, $gfpoly, $fcr, $prim, $nroots, $pad) + { + foreach(self::$items as $rs) { + if($rs->pad != $pad) continue; + if($rs->nroots != $nroots) continue; + if($rs->mm != $symsize) continue; + if($rs->gfpoly != $gfpoly) continue; + if($rs->fcr != $fcr) continue; + if($rs->prim != $prim) continue; + + return $rs; + } + + $rs = QRrsItem::init_rs_char($symsize, $gfpoly, $fcr, $prim, $nroots, $pad); + array_unshift(self::$items, $rs); + + return $rs; + } + } + + + +//---- qrmask.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Masking + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + define('N1', 3); + define('N2', 3); + define('N3', 40); + define('N4', 10); + + class QRmask { + + public $runLength = array(); + + //---------------------------------------------------------------------- + public function __construct() + { + $this->runLength = array_fill(0, QRSPEC_WIDTH_MAX + 1, 0); + } + + //---------------------------------------------------------------------- + public function writeFormatInformation($width, &$frame, $mask, $level) + { + $blacks = 0; + $format = QRspec::getFormatInfo($mask, $level); + + for($i=0; $i<8; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[8][$width - 1 - $i] = chr($v); + if($i < 6) { + $frame[$i][8] = chr($v); + } else { + $frame[$i + 1][8] = chr($v); + } + $format = $format >> 1; + } + + for($i=0; $i<7; $i++) { + if($format & 1) { + $blacks += 2; + $v = 0x85; + } else { + $v = 0x84; + } + + $frame[$width - 7 + $i][8] = chr($v); + if($i == 0) { + $frame[8][7] = chr($v); + } else { + $frame[8][6 - $i] = chr($v); + } + + $format = $format >> 1; + } + + return $blacks; + } + + //---------------------------------------------------------------------- + public function mask0($x, $y) { return ($x+$y)&1; } + public function mask1($x, $y) { return ($y&1); } + public function mask2($x, $y) { return ($x%3); } + public function mask3($x, $y) { return ($x+$y)%3; } + public function mask4($x, $y) { return (((int)($y/2))+((int)($x/3)))&1; } + public function mask5($x, $y) { return (($x*$y)&1)+($x*$y)%3; } + public function mask6($x, $y) { return ((($x*$y)&1)+($x*$y)%3)&1; } + public function mask7($x, $y) { return ((($x*$y)%3)+(($x+$y)&1))&1; } + + //---------------------------------------------------------------------- + private function generateMaskNo($maskNo, $width, $frame) + { + $bitMask = array_fill(0, $width, array_fill(0, $width, 0)); + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if(ord($frame[$y][$x]) & 0x80) { + $bitMask[$y][$x] = 0; + } else { + $maskFunc = call_user_func(array($this, 'mask'.$maskNo), $x, $y); + $bitMask[$y][$x] = ($maskFunc == 0)?1:0; + } + + } + } + + return $bitMask; + } + + //---------------------------------------------------------------------- + public static function serial($bitFrame) + { + $codeArr = array(); + + foreach ($bitFrame as $line) + $codeArr[] = join('', $line); + + return gzcompress(join("\n", $codeArr), 9); + } + + //---------------------------------------------------------------------- + public static function unserial($code) + { + $codeArr = array(); + + $codeLines = explode("\n", gzuncompress($code)); + foreach ($codeLines as $line) + $codeArr[] = str_split($line); + + return $codeArr; + } + + //---------------------------------------------------------------------- + public function makeMaskNo($maskNo, $width, $s, &$d, $maskGenOnly = false) + { + $b = 0; + $bitMask = array(); + + $fileName = QR_CACHE_DIR.'mask_'.$maskNo.DIRECTORY_SEPARATOR.'mask_'.$width.'_'.$maskNo.'.dat'; + + if (QR_CACHEABLE) { + if (file_exists($fileName)) { + $bitMask = self::unserial(file_get_contents($fileName)); + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + if (!file_exists(QR_CACHE_DIR.'mask_'.$maskNo)) + mkdir(QR_CACHE_DIR.'mask_'.$maskNo); + file_put_contents($fileName, self::serial($bitMask)); + } + } else { + $bitMask = $this->generateMaskNo($maskNo, $width, $s, $d); + } + + if ($maskGenOnly) + return; + + $d = $s; + + for($y=0; $y<$width; $y++) { + for($x=0; $x<$width; $x++) { + if($bitMask[$y][$x] == 1) { + $d[$y][$x] = chr(ord($s[$y][$x]) ^ (int)$bitMask[$y][$x]); + } + $b += (int)(ord($d[$y][$x]) & 1); + } + } + + return $b; + } + + //---------------------------------------------------------------------- + public function makeMask($width, $frame, $maskNo, $level) + { + $masked = array_fill(0, $width, str_repeat("\0", $width)); + $this->makeMaskNo($maskNo, $width, $frame, $masked); + $this->writeFormatInformation($width, $masked, $maskNo, $level); + + return $masked; + } + + //---------------------------------------------------------------------- + public function calcN1N3($length) + { + $demerit = 0; + + for($i=0; $i<$length; $i++) { + + if($this->runLength[$i] >= 5) { + $demerit += (N1 + ($this->runLength[$i] - 5)); + } + if($i & 1) { + if(($i >= 3) && ($i < ($length-2)) && ($this->runLength[$i] % 3 == 0)) { + $fact = (int)($this->runLength[$i] / 3); + if(($this->runLength[$i-2] == $fact) && + ($this->runLength[$i-1] == $fact) && + ($this->runLength[$i+1] == $fact) && + ($this->runLength[$i+2] == $fact)) { + if(($this->runLength[$i-3] < 0) || ($this->runLength[$i-3] >= (4 * $fact))) { + $demerit += N3; + } else if((($i+3) >= $length) || ($this->runLength[$i+3] >= (4 * $fact))) { + $demerit += N3; + } + } + } + } + } + return $demerit; + } + + //---------------------------------------------------------------------- + public function evaluateSymbol($width, $frame) + { + $head = 0; + $demerit = 0; + + for($y=0; $y<$width; $y++) { + $head = 0; + $this->runLength[0] = 1; + + $frameY = $frame[$y]; + + if ($y>0) + $frameYM = $frame[$y-1]; + + for($x=0; $x<$width; $x++) { + if(($x > 0) && ($y > 0)) { + $b22 = ord($frameY[$x]) & ord($frameY[$x-1]) & ord($frameYM[$x]) & ord($frameYM[$x-1]); + $w22 = ord($frameY[$x]) | ord($frameY[$x-1]) | ord($frameYM[$x]) | ord($frameYM[$x-1]); + + if(($b22 | ($w22 ^ 1))&1) { + $demerit += N2; + } + } + if(($x == 0) && (ord($frameY[$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($x > 0) { + if((ord($frameY[$x]) ^ ord($frameY[$x-1])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + for($x=0; $x<$width; $x++) { + $head = 0; + $this->runLength[0] = 1; + + for($y=0; $y<$width; $y++) { + if($y == 0 && (ord($frame[$y][$x]) & 1)) { + $this->runLength[0] = -1; + $head = 1; + $this->runLength[$head] = 1; + } else if($y > 0) { + if((ord($frame[$y][$x]) ^ ord($frame[$y-1][$x])) & 1) { + $head++; + $this->runLength[$head] = 1; + } else { + $this->runLength[$head]++; + } + } + } + + $demerit += $this->calcN1N3($head+1); + } + + return $demerit; + } + + + //---------------------------------------------------------------------- + public function mask($width, $frame, $level) + { + $minDemerit = PHP_INT_MAX; + $bestMaskNum = 0; + $bestMask = array(); + + $checked_masks = array(0,1,2,3,4,5,6,7); + + if (QR_FIND_FROM_RANDOM !== false) { + + $howManuOut = 8-(QR_FIND_FROM_RANDOM % 9); + for ($i = 0; $i < $howManuOut; $i++) { + $remPos = rand (0, count($checked_masks)-1); + unset($checked_masks[$remPos]); + $checked_masks = array_values($checked_masks); + } + + } + + $bestMask = $frame; + + foreach($checked_masks as $i) { + $mask = array_fill(0, $width, str_repeat("\0", $width)); + + $demerit = 0; + $blacks = 0; + $blacks = $this->makeMaskNo($i, $width, $frame, $mask); + $blacks += $this->writeFormatInformation($width, $mask, $i, $level); + $blacks = (int)(100 * $blacks / ($width * $width)); + $demerit = (int)((int)(abs($blacks - 50) / 5) * N4); + $demerit += $this->evaluateSymbol($width, $mask); + + if($demerit < $minDemerit) { + $minDemerit = $demerit; + $bestMask = $mask; + $bestMaskNum = $i; + } + } + + return $bestMask; + } + + //---------------------------------------------------------------------- + } + + + + +//---- qrencode.php ----------------------------- + + + + +/* + * PHP QR Code encoder + * + * Main encoder classes. + * + * Based on libqrencode C library distributed under LGPL 2.1 + * Copyright (C) 2006, 2007, 2008, 2009 Kentaro Fukuchi + * + * PHP QR Code is distributed under LGPL 3 + * Copyright (C) 2010 Dominik Dzienia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + class QRrsblock { + public $dataLength; + public $data = array(); + public $eccLength; + public $ecc = array(); + + public function __construct($dl, $data, $el, &$ecc, QRrsItem $rs) + { + $rs->encode_rs_char($data, $ecc); + + $this->dataLength = $dl; + $this->data = $data; + $this->eccLength = $el; + $this->ecc = $ecc; + } + }; + + //########################################################################## + + class QRrawcode { + public $version; + public $datacode = array(); + public $ecccode = array(); + public $blocks; + public $rsblocks = array(); //of RSblock + public $count; + public $dataLength; + public $eccLength; + public $b1; + + //---------------------------------------------------------------------- + public function __construct(QRinput $input) + { + $spec = array(0,0,0,0,0); + + $this->datacode = $input->getByteStream(); + if(is_null($this->datacode)) { + throw new Exception('null imput string'); + } + + QRspec::getEccSpec($input->getVersion(), $input->getErrorCorrectionLevel(), $spec); + + $this->version = $input->getVersion(); + $this->b1 = QRspec::rsBlockNum1($spec); + $this->dataLength = QRspec::rsDataLength($spec); + $this->eccLength = QRspec::rsEccLength($spec); + $this->ecccode = array_fill(0, $this->eccLength, 0); + $this->blocks = QRspec::rsBlockNum($spec); + + $ret = $this->init($spec); + if($ret < 0) { + throw new Exception('block alloc error'); + return null; + } + + $this->count = 0; + } + + //---------------------------------------------------------------------- + public function init(array $spec) + { + $dl = QRspec::rsDataCodes1($spec); + $el = QRspec::rsEccCodes1($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + + $blockNo = 0; + $dataPos = 0; + $eccPos = 0; + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + if(QRspec::rsBlockNum2($spec) == 0) + return 0; + + $dl = QRspec::rsDataCodes2($spec); + $el = QRspec::rsEccCodes2($spec); + $rs = QRrs::init_rs(8, 0x11d, 0, 1, $el, 255 - $dl - $el); + + if($rs == NULL) return -1; + + for($i=0; $iecccode,$eccPos); + $this->rsblocks[$blockNo] = new QRrsblock($dl, array_slice($this->datacode, $dataPos), $el, $ecc, $rs); + $this->ecccode = array_merge(array_slice($this->ecccode,0, $eccPos), $ecc); + + $dataPos += $dl; + $eccPos += $el; + $blockNo++; + } + + return 0; + } + + //---------------------------------------------------------------------- + public function getCode() + { + $ret; + + if($this->count < $this->dataLength) { + $row = $this->count % $this->blocks; + $col = $this->count / $this->blocks; + if($col >= $this->rsblocks[0]->dataLength) { + $row += $this->b1; + } + $ret = $this->rsblocks[$row]->data[$col]; + } else if($this->count < $this->dataLength + $this->eccLength) { + $row = ($this->count - $this->dataLength) % $this->blocks; + $col = ($this->count - $this->dataLength) / $this->blocks; + $ret = $this->rsblocks[$row]->ecc[$col]; + } else { + return 0; + } + $this->count++; + + return $ret; + } + } + + //########################################################################## + + class QRcode { + + public $version; + public $width; + public $data; + + //---------------------------------------------------------------------- + public function encodeMask(QRinput $input, $mask) + { + if($input->getVersion() < 0 || $input->getVersion() > QRSPEC_VERSION_MAX) { + throw new Exception('wrong version'); + } + if($input->getErrorCorrectionLevel() > QR_ECLEVEL_H) { + throw new Exception('wrong level'); + } + + $raw = new QRrawcode($input); + + QRtools::markTime('after_raw'); + + $version = $raw->version; + $width = QRspec::getWidth($version); + $frame = QRspec::newFrame($version); + + $filler = new FrameFiller($width, $frame); + if(is_null($filler)) { + return NULL; + } + + // inteleaved data and ecc codes + for($i=0; $i<$raw->dataLength + $raw->eccLength; $i++) { + $code = $raw->getCode(); + $bit = 0x80; + for($j=0; $j<8; $j++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02 | (($bit & $code) != 0)); + $bit = $bit >> 1; + } + } + + QRtools::markTime('after_filler'); + + unset($raw); + + // remainder bits + $j = QRspec::getRemainder($version); + for($i=0; $i<$j; $i++) { + $addr = $filler->next(); + $filler->setFrameAt($addr, 0x02); + } + + $frame = $filler->frame; + unset($filler); + + + // masking + $maskObj = new QRmask(); + if($mask < 0) { + + if (QR_FIND_BEST_MASK) { + $masked = $maskObj->mask($width, $frame, $input->getErrorCorrectionLevel()); + } else { + $masked = $maskObj->makeMask($width, $frame, (intval(QR_DEFAULT_MASK) % 8), $input->getErrorCorrectionLevel()); + } + } else { + $masked = $maskObj->makeMask($width, $frame, $mask, $input->getErrorCorrectionLevel()); + } + + if($masked == NULL) { + return NULL; + } + + QRtools::markTime('after_mask'); + + $this->version = $version; + $this->width = $width; + $this->data = $masked; + + return $this; + } + + //---------------------------------------------------------------------- + public function encodeInput(QRinput $input) + { + return $this->encodeMask($input, -1); + } + + //---------------------------------------------------------------------- + public function encodeString8bit($string, $version, $level) + { + if(string == NULL) { + throw new Exception('empty string!'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = $input->append($input, QR_MODE_8, strlen($string), str_split($string)); + if($ret < 0) { + unset($input); + return NULL; + } + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public function encodeString($string, $version, $level, $hint, $casesensitive) + { + + if($hint != QR_MODE_8 && $hint != QR_MODE_KANJI) { + throw new Exception('bad hint'); + return NULL; + } + + $input = new QRinput($version, $level); + if($input == NULL) return NULL; + + $ret = QRsplit::splitStringToQRinput($string, $input, $hint, $casesensitive); + if($ret < 0) { + return NULL; + } + + return $this->encodeInput($input); + } + + //---------------------------------------------------------------------- + public static function png($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4, $saveandprint=false) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodePNG($text, $outfile, $saveandprint=false); + } + + //---------------------------------------------------------------------- + public static function text($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encode($text, $outfile); + } + + //---------------------------------------------------------------------- + public static function raw($text, $outfile = false, $level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = QRencode::factory($level, $size, $margin); + return $enc->encodeRAW($text, $outfile); + } + } + + //########################################################################## + + class FrameFiller { + + public $width; + public $frame; + public $x; + public $y; + public $dir; + public $bit; + + //---------------------------------------------------------------------- + public function __construct($width, &$frame) + { + $this->width = $width; + $this->frame = $frame; + $this->x = $width - 1; + $this->y = $width - 1; + $this->dir = -1; + $this->bit = -1; + } + + //---------------------------------------------------------------------- + public function setFrameAt($at, $val) + { + $this->frame[$at['y']][$at['x']] = chr($val); + } + + //---------------------------------------------------------------------- + public function getFrameAt($at) + { + return ord($this->frame[$at['y']][$at['x']]); + } + + //---------------------------------------------------------------------- + public function next() + { + do { + + if($this->bit == -1) { + $this->bit = 0; + return array('x'=>$this->x, 'y'=>$this->y); + } + + $x = $this->x; + $y = $this->y; + $w = $this->width; + + if($this->bit == 0) { + $x--; + $this->bit++; + } else { + $x++; + $y += $this->dir; + $this->bit--; + } + + if($this->dir < 0) { + if($y < 0) { + $y = 0; + $x -= 2; + $this->dir = 1; + if($x == 6) { + $x--; + $y = 9; + } + } + } else { + if($y == $w) { + $y = $w - 1; + $x -= 2; + $this->dir = -1; + if($x == 6) { + $x--; + $y -= 8; + } + } + } + if($x < 0 || $y < 0) return null; + + $this->x = $x; + $this->y = $y; + + } while(ord($this->frame[$y][$x]) & 0x80); + + return array('x'=>$x, 'y'=>$y); + } + + } ; + + //########################################################################## + + class QRencode { + + public $casesensitive = true; + public $eightbit = false; + + public $version = 0; + public $size = 3; + public $margin = 4; + + public $structured = 0; // not supported yet + + public $level = QR_ECLEVEL_L; + public $hint = QR_MODE_8; + + //---------------------------------------------------------------------- + public static function factory($level = QR_ECLEVEL_L, $size = 3, $margin = 4) + { + $enc = new QRencode(); + $enc->size = $size; + $enc->margin = $margin; + + switch ($level.'') { + case '0': + case '1': + case '2': + case '3': + $enc->level = $level; + break; + case 'l': + case 'L': + $enc->level = QR_ECLEVEL_L; + break; + case 'm': + case 'M': + $enc->level = QR_ECLEVEL_M; + break; + case 'q': + case 'Q': + $enc->level = QR_ECLEVEL_Q; + break; + case 'h': + case 'H': + $enc->level = QR_ECLEVEL_H; + break; + } + + return $enc; + } + + //---------------------------------------------------------------------- + public function encodeRAW($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + return $code->data; + } + + //---------------------------------------------------------------------- + public function encode($intext, $outfile = false) + { + $code = new QRcode(); + + if($this->eightbit) { + $code->encodeString8bit($intext, $this->version, $this->level); + } else { + $code->encodeString($intext, $this->version, $this->level, $this->hint, $this->casesensitive); + } + + QRtools::markTime('after_encode'); + + if ($outfile!== false) { + file_put_contents($outfile, join("\n", QRtools::binarize($code->data))); + } else { + return QRtools::binarize($code->data); + } + } + + //---------------------------------------------------------------------- + public function encodePNG($intext, $outfile = false,$saveandprint=false) + { + try { + + ob_start(); + $tab = $this->encode($intext); + $err = ob_get_contents(); + ob_end_clean(); + + if ($err != '') + QRtools::log($outfile, $err); + + $maxSize = (int)(QR_PNG_MAXIMUM_SIZE / (count($tab)+2*$this->margin)); + + QRimage::png($tab, $outfile, min(max(1, $this->size), $maxSize), $this->margin,$saveandprint); + + } catch (Exception $e) { + + QRtools::log($outfile, $e->getMessage()); + + } + } + } + + diff --git a/include/totp.class.php b/include/totp.class.php new file mode 100644 index 000000000..fcab5a5a1 --- /dev/null +++ b/include/totp.class.php @@ -0,0 +1,115 @@ + 30-second intervals since 1970 at the moment T + return self::generateCodeFromTimestamp($secret, $timestamp); + } + + /** + * Verify TOTP Code + * + * @param string $code Digits 6 TOTP Code + * @param string $secret Encoded base32 secret + * @param int $timestamp timestamp used in second (default: 30) + * @param int $check_interval Number of 30s steps to check before/after current (default: 1) + * @return bool + */ + public static function verifyCode($code, $secret, $timestamp = 30, $check_interval = 1) + { + $timestamp = floor(time() / $timestamp); + + // generate a totp code for 30s intervals + // following or preceding the current one and check it + for ($i=-$check_interval; $i <= $check_interval; $i++) + { + $interval_timestamp = $timestamp + $i; + $generated_code = self::generateCodeFromTimestamp($secret, $interval_timestamp); + if (hash_equals($generated_code, $code)) + { + return true; + } + } + + return false; + } +} diff --git a/language/en_UK/common.lang.php b/language/en_UK/common.lang.php index 5cb17cf07..fd1ac33d3 100644 --- a/language/en_UK/common.lang.php +++ b/language/en_UK/common.lang.php @@ -524,3 +524,8 @@ $lang['Your API key will expire in %d days.'] = 'Your API key will expire in %d $lang['To continue using the API, please renew your key before it expires.'] = 'To continue using the API, please renew your key before it expires.'; $lang['You can manage your API keys in your account settings.'] = 'You can manage your API keys in your account settings.'; $lang['Expert mode'] = 'Expert mode'; +$lang['An email has been sent with a verification code'] = 'An email has been sent with a verification code'; +$lang['If you do not receive the email, please contact your webmaster.'] = 'If you do not receive the email, please contact your webmaster.'; +$lang['Verification code'] = 'Verification code'; +$lang['Verify'] = 'Verify'; +$lang['Invalid verification code'] = 'Invalid verification code'; diff --git a/language/fr_FR/common.lang.php b/language/fr_FR/common.lang.php index 485234611..e2a547199 100644 --- a/language/fr_FR/common.lang.php +++ b/language/fr_FR/common.lang.php @@ -523,3 +523,8 @@ $lang['Your API key will expire in %d days.'] = 'Votre clé API expirera dans %d $lang['To continue using the API, please renew your key before it expires.'] = 'Pour continuer à utiliser l\'API, veuillez renouveler votre clé avant son expiration.'; $lang['You can manage your API keys in your account settings.'] = 'Vous pouvez gérer vos clés API dans les paramètres de votre compte.'; $lang['Expert mode'] = 'Mode expert'; +$lang['An email has been sent with a verification code'] = 'Un e-mail contenant un code de vérification vous a été envoyé'; +$lang['If you do not receive the email, please contact your webmaster.'] = 'Si vous ne recevez pas cet e-mail, veuillez contacter votre webmaster.'; +$lang['Verification code'] = 'Code de vérification'; +$lang['Verify'] = 'Vérifier'; +$lang['Invalid verification code'] = 'Code de vérification invalide'; diff --git a/password.php b/password.php index 26e3b3ab8..0a9230379 100644 --- a/password.php +++ b/password.php @@ -22,7 +22,7 @@ check_status(ACCESS_FREE); trigger_notify('loc_begin_password'); -check_input_parameter('action', $_GET, false, '/^(lost|reset|lost_end|reset_end|none)$/'); +check_input_parameter('action', $_GET, false, '/^(lost|reset|lost_code|lost_end|reset_end|none)$/'); // +-----------------------------------------------------------------------+ // | Functions | @@ -30,71 +30,160 @@ check_input_parameter('action', $_GET, false, '/^(lost|reset|lost_end|reset_end| /** * checks the validity of input parameters, fills $page['errors'] and - * $page['infos'] and send an email with confirmation link + * $page['infos'] and send an email with the verification code + * + * @return bool + */ +function process_verification_code() +{ + global $page, $conf, $logger; + + if (isset($_SESSION['reset_password_code'])) + { + return true; + } + + // empty param + $username_or_email = trim($_POST['username_or_email']); + if (empty($username_or_email)) + { + $page['errors']['password_form_error'] = l10n('Invalid username or email'); + return false; + } + + // retrievies user by email is not try by username + $user_id = get_userid_by_email($username_or_email); + + if (!is_numeric($user_id)) + { + $user_id = get_userid($username_or_email); + } + + // when no user is found, we assign guest_id instead of stopping. + // this lets the function behave identically for unknown users, + // preventing username/email enumeration through timing or responses. + $is_user_founded = is_numeric($user_id); + if (!$is_user_founded) + { + $user_id = $conf['guest_id']; + } + + $userdata = getuserdata($user_id, false); + + // check if we want to skip email sending + // if user is guest, generic or doesn't have email + $status = $userdata['status']; + $skip_mail = !$is_user_founded or is_a_guest($status) or is_generic($status) or empty($userdata['email']); + + // send mail with verification code to user + switch_lang_to($userdata['language']); + $user_code = generate_user_code(); + $template_mail = pwg_generate_code_verification_mail($user_code['code']); + if (!$skip_mail) + { + $mail_send = pwg_mail($userdata['email'], $template_mail); + // pwg_activity('user', $userdata['id'], 'reset_password_code', array( + // 'ip' => $_SERVER['REMOTE_ADDR'], + // 'agent' => $_SERVER['HTTP_USER_AGENT'], + // 'is_mail_sent' => $mail_send + // )); + } + switch_lang_back(); + + $_SESSION['reset_password_code'] = [ + 'secret' => $user_code['secret'], + 'attempts' => 0, + 'user_id' => $is_user_founded ? $user_id : null, + 'created_at' => time(), + 'ttl' => min($conf['password_reset_code_duration'], 900) // max 15 min + ]; + + return true; +} + +/** + * checks the validity of input parameters, fills $page['errors'] and + * $page['infos'] and send an email with reset link * * @return bool (true if email was sent, false otherwise) */ function process_password_request() { global $page, $conf; - - if (empty($_POST['username_or_email'])) + + $state = $_SESSION['reset_password_code'] ?? null; + if (!$state) { - $page['errors']['password_form_error'] = l10n('Invalid username or email'); + return true; // fallback line 366 + } + + // check expired + if (time() > $state['created_at'] + $state['ttl']) + { + unset($_SESSION['reset_password_code']); + $page['errors']['password_form_error'] = l10n('Code expired'); return false; } - - $user_id = get_userid_by_email($_POST['username_or_email']); + + $_SESSION['reset_password_code']['attempts']++; - if (!is_numeric($user_id)) + $is_valid = true; + $user_code = trim($_POST['user_code'] ?? ''); + + if ( + empty($user_code) // empty user code + || !preg_match('/^\d{6}$/', $user_code) // check digit 6 + || !verify_user_code($state['secret'], $user_code)) // verify user code { - $user_id = get_userid($_POST['username_or_email']); + $is_valid = false; } - if (!is_numeric($user_id)) + if (!$is_valid) { - $page['errors']['password_form_error'] = l10n('Invalid username or email'); + if ($_SESSION['reset_password_code']['attempts'] >= 3) + { + unset($_SESSION['reset_password_code']); + $page['errors']['login_page_error'] = l10n('Too many attempts'); + return false; + } + + $page['errors']['password_form_error'] = l10n('Invalid verification code'); return false; } - $userdata = getuserdata($user_id, false); + // verify code success + $user_id = $state['user_id']; + unset($_SESSION['reset_password_code']); - // password request is not possible for guest/generic users - $status = $userdata['status']; - if (is_a_guest($status) or is_generic($status)) + if (empty($user_id)) + { + $page['errors']['password_form_error'] = l10n('Invalid verification code'); + return false; + } + + $userdata = getuserdata($user_id); + $status = $userdata['status'] ?? null; + + // fallback check: don't send mail when user is guest, generic or doesn't have email + if (is_a_guest($status) || is_generic($status) || empty($userdata['email'])) { $page['errors']['password_form_error'] = l10n('Password reset is not allowed for this user'); return false; } - if (empty($userdata['email'])) - { - $page['errors']['password_form_error'] = l10n( - 'User "%s" has no email address, password reset is not possible', - $userdata['username'] - ); - return false; - } - $generate_link = generate_password_link($user_id); - - // $userdata['activation_key'] = $generate_link['activation_key']; - switch_lang_to($userdata['language']); $email_params = pwg_generate_reset_password_mail($userdata['username'], $generate_link['password_link'], $conf['gallery_title'], $generate_link['time_validation']); $send_email = pwg_mail($userdata['email'], $email_params); switch_lang_back(); - if ($send_email) - { - $page['infos'][] = l10n('Check your email for the confirmation link'); - return true; - } - else - { - $page['errors']['password_page_error'] = l10n('Error sending email'); - return false; - } + // pwg_activity('user', $userdata['id'], 'reset_password_link', array( + // 'ip' => $_SERVER['REMOTE_ADDR'], + // 'agent' => $_SERVER['HTTP_USER_AGENT'], + // 'is_mail_sent' => $send_email + // )); + + return true; } /** @@ -199,9 +288,19 @@ if (isset($_POST['submit'])) check_pwg_token(); if ('lost' == $_GET['action']) + { + if (process_verification_code()) + { + $page['infos'][] = l10n('An email has been sent with a verification code'); + $page['action'] = 'lost_code'; + } + } + + if ('lost_code' == $_GET['action']) { if (process_password_request()) { + $page['infos'][] = l10n('An email has been sent with a link to reset your password'); $page['action'] = 'lost_end'; } } @@ -253,7 +352,7 @@ if (!isset($page['action'])) { $page['action'] = 'lost'; } - elseif (in_array($_GET['action'], array('lost', 'reset', 'none'))) + elseif (in_array($_GET['action'], array('lost', 'lost_code', 'reset', 'none'))) { $page['action'] = $_GET['action']; } @@ -269,6 +368,16 @@ if ('lost' == $page['action'] and !is_a_guest()) redirect(get_gallery_home_url()); } +if ('lost_code' == $page['action'] and !isset($_SESSION['reset_password_code'])) +{ + redirect(get_gallery_home_url(). 'identification.php'); +} + +if ('lost' == $page['action'] and isset($_SESSION['reset_password_code'])) +{ + $page['action'] = 'lost_code'; +} + // +-----------------------------------------------------------------------+ // | template initialization | // +-----------------------------------------------------------------------+ diff --git a/themes/default/template/password.tpl b/themes/default/template/password.tpl index e9389ff9d..ae0fd958f 100644 --- a/themes/default/template/password.tpl +++ b/themes/default/template/password.tpl @@ -27,6 +27,17 @@

+ {elseif $action eq 'lost_code'} +
+
{"If you do not receive the email, please contact your webmaster."|translate}
+ + +

+
{elseif $action eq 'reset'}
@@ -60,6 +71,8 @@ {literal}try{document.getElementById('username_or_email').focus();}catch(e){}{/literal} {elseif $action eq 'reset'} {literal}try{document.getElementById('use_new_pwd').focus();}catch(e){}{/literal} +{elseif $action eq 'lost_code'} +{literal}try{document.getElementById('user_code').focus();}catch(e){}{/literal} {/if} diff --git a/themes/standard_pages/template/password.tpl b/themes/standard_pages/template/password.tpl index f8e28e4f7..9d135e7b4 100644 --- a/themes/standard_pages/template/password.tpl +++ b/themes/standard_pages/template/password.tpl @@ -37,14 +37,14 @@
-{if $action eq 'lost' or $action eq 'reset'} +{if $action eq 'lost' or $action eq 'reset' or $action eq 'lost_code'}

{if !isset($is_first_login)}{'Forgot your password?'|translate}{else}{'Welcome !'|translate}
{'It\'s your first login !'|translate}{/if}

{if $action eq 'lost'} -

{'Please enter your username or email address.'|@translate}
{'You will receive a link to create a new password via email.'|@translate}

+

{'Please enter your username or email address.'|@translate} {'You will receive a link to create a new password via email.'|@translate}

@@ -105,6 +105,24 @@
+ {elseif $action eq 'lost_code'} + {'An email has been sent with a verification code'|translate} +
+ +
+ + +
+

{'must not be empty'|translate}

+
+ +
+ + {if isset($errors['password_form_error'])} +

{$errors['password_form_error']}

+ {/if} +

{"If you do not receive the email, please contact your webmaster."|translate}

+
{/if}
From f0f4b30ce2e83fb14ce77684cc19bd326789e190 Mon Sep 17 00:00:00 2001 From: plegall Date: Tue, 28 Oct 2025 16:35:05 +0100 Subject: [PATCH 057/149] search filters: avoid to write 3 times the same default configuration --- admin/configuration.php | 23 +++++++---------------- include/config_default.inc.php | 25 +++++++++++++++++++++++++ include/search_filters.inc.php | 9 +-------- install/config.sql | 1 - install/db/178-database.php | 6 +----- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/admin/configuration.php b/admin/configuration.php index 4d04f8a98..e29fa32f1 100644 --- a/admin/configuration.php +++ b/admin/configuration.php @@ -112,21 +112,12 @@ $display_info_checkboxes = array( 'rating_score', ); -$filters_names_checkboxes = array( - 'words', - 'tags', - 'post_date', - 'creation_date', - 'album', - 'author', - 'added_by', - 'file_type', - 'ratio', - 'rating', - 'file_size', - 'height', - 'width' -); +if (!isset($conf['filters_views'])) +{ + conf_update_param('filters_views', $conf['default_filters_views'], true); +} + +$filters_names_checkboxes = array_diff(array_keys(safe_unserialize($conf['filters_views'])), array('last_filters_conf')); // image order management $sort_fields = array( @@ -701,7 +692,7 @@ switch ($page['section']) $template->assign( 'search', array( - 'filters_views' => unserialize($conf['filters_views']), + 'filters_views' => safe_unserialize($conf['filters_views']), 'filters_names' => $filters_names, ), ); diff --git a/include/config_default.inc.php b/include/config_default.inc.php index 8339a4326..0dd0a84d1 100644 --- a/include/config_default.inc.php +++ b/include/config_default.inc.php @@ -1027,12 +1027,37 @@ $conf['batch_manager_images_per_page_unit'] = 5; // how many missing md5sum should Piwigo compute at once. $conf['checksum_compute_blocksize'] = 50; +// +-----------------------------------------------------------------------+ +// | Search | +// +-----------------------------------------------------------------------+ + // quicksearch engine: include all photos from sub-albums of any matching // album. For example, if search is "bear", then we display photos from // "bear/grizzly". When value changed, delete database cache files in // _data/cache directory $conf['quick_search_include_sub_albums'] = false; +// default configuration for search filters. It will then be configurable +// with the configuration page. Having this setting in this file avoids to +// duplicate it in several files +$conf['default_filters_views'] = array( + 'words' => ['access'=>'everybody', 'default'=>true], + 'tags' => ['access'=>'everybody', 'default'=>false], + 'post_date' => ['access'=>'everybody', 'default'=>false], + 'creation_date' => ['access'=>'everybody', 'default'=>true], + 'album' => ['access'=>'everybody', 'default'=>true], + 'author' => ['access'=>'everybody', 'default'=>false], + 'added_by' => ['access'=>'everybody', 'default'=>false], + 'file_type' => ['access'=>'everybody', 'default'=>false], + 'ratio' => ['access'=>'everybody', 'default'=>false], + 'rating' => ['access'=>'everybody', 'default'=>false], + 'file_size' => ['access'=>'everybody', 'default'=>false], + 'height' => ['access'=>'everybody', 'default'=>false], + 'width' => ['access'=>'everybody', 'default'=>false], + + 'last_filters_conf' => true, +); + // +-----------------------------------------------------------------------+ // | log | // +-----------------------------------------------------------------------+ diff --git a/include/search_filters.inc.php b/include/search_filters.inc.php index f22e2976c..7ffb65636 100644 --- a/include/search_filters.inc.php +++ b/include/search_filters.inc.php @@ -6,14 +6,7 @@ // | file that was distributed with this source code. | // +-----------------------------------------------------------------------+ -if (isset($conf['filters_views'])) -{ - $filters_views = unserialize($conf['filters_views']); -} -else -{ - $filters_views = unserialize('a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}'); -} +$filters_views = safe_unserialize(conf_get_param('filters_views', $conf['default_filters_views'])); $template->assign('display_filter', $filters_views); diff --git a/install/config.sql b/install/config.sql index a12d2b3f0..1dc6915ea 100644 --- a/install/config.sql +++ b/install/config.sql @@ -80,4 +80,3 @@ INSERT INTO piwigo_config (param,value) VALUES ('index_search_in_set_action','tr INSERT INTO piwigo_config (param,value) VALUES ('upload_detect_duplicate','true'); INSERT INTO piwigo_config (param,value) VALUES ('webmaster_id','1'); INSERT INTO piwigo_config (param,value) VALUES ('use_standard_pages','true'); -INSERT INTO piwigo_config (param,value,comment) VALUES ('filters_views','a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}','Filters displays configuration'); \ No newline at end of file diff --git a/install/db/178-database.php b/install/db/178-database.php index 9aa85dece..29c32da7a 100644 --- a/install/db/178-database.php +++ b/install/db/178-database.php @@ -13,11 +13,7 @@ if (!defined('PHPWG_ROOT_PATH')) $upgrade_description = 'add config parameters to the gallery filters'; -// Add line of conf to help with the display of filters and default filters in the gallery -conf_update_param( - 'filters_views', - 'a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}' -); +conf_update_param('filters_views', $conf['default_filters_views']); echo "\n".$upgrade_description."\n"; From b416bdb7c975ccf2bdb467bca8b38c29259288ba Mon Sep 17 00:00:00 2001 From: plegall Date: Tue, 28 Oct 2025 18:39:18 +0100 Subject: [PATCH 058/149] fixes #2417 ability to hide the new filter "expert mode" ... and removes 2 other duplicates of the filters_views configuration --- admin/configuration.php | 5 +- .../default/template/configuration_search.tpl | 30 +++++------- admin/themes/default/theme.css | 4 +- include/config_default.inc.php | 1 + include/functions_search.inc.php | 12 +---- include/search_filters.inc.php | 9 +++- install/db/178-database.php | 4 +- install/db/181-database.php | 33 +++++++++++++ language/en_UK/admin.lang.php | 1 + language/fr_FR/admin.lang.php | 1 + search.php | 46 ++++++++----------- .../template/include/search_filters.inc.tpl | 3 +- 12 files changed, 85 insertions(+), 64 deletions(-) create mode 100644 install/db/181-database.php diff --git a/admin/configuration.php b/admin/configuration.php index e29fa32f1..bbc62cae2 100644 --- a/admin/configuration.php +++ b/admin/configuration.php @@ -117,7 +117,7 @@ if (!isset($conf['filters_views'])) conf_update_param('filters_views', $conf['default_filters_views'], true); } -$filters_names_checkboxes = array_diff(array_keys(safe_unserialize($conf['filters_views'])), array('last_filters_conf')); +$filters_names_checkboxes = array_values(array_diff(array_keys(safe_unserialize($conf['filters_views'])), array('last_filters_conf'))); // image order management $sort_fields = array( @@ -688,12 +688,11 @@ switch ($page['section']) } case 'search': { - $filters_names = $filters_names_checkboxes; $template->assign( 'search', array( 'filters_views' => safe_unserialize($conf['filters_views']), - 'filters_names' => $filters_names, + 'filters_names' => $filters_names_checkboxes, ), ); $template->assign('SHOW_FILTER_RATINGS', $conf['rate']); diff --git a/admin/themes/default/template/configuration_search.tpl b/admin/themes/default/template/configuration_search.tpl index d5d8aaed8..42b36a0d2 100644 --- a/admin/themes/default/template/configuration_search.tpl +++ b/admin/themes/default/template/configuration_search.tpl @@ -2,22 +2,7 @@ {footer_script} -filters_names = -[ - 'words', - 'tags', - 'post_date', - 'creation_date', - 'album', - 'author', - 'added_by', - 'file_type', - 'ratio', - 'rating', - 'file_size', - 'height', - 'width' -]; +const filters_names = {$search.filters_names|json_encode}; for(const filter_name of filters_names){ if(!$("input#"+filter_name+"Filters").is(':checked')){ @@ -91,6 +76,8 @@ for(const filter_name of filters_names){ > {if $filter_name == 'words'} {'Search for words'|translate} + {else if $filter_name == 'expert'} + {'Expert mode'|translate} {else if $filter_name == 'file_size'} {'Filesize'|translate} {else} @@ -100,7 +87,7 @@ for(const filter_name of filters_names){
@@ -225,7 +212,14 @@ for(const filter_name of filters_names){ {/if} hidden/> - +
diff --git a/admin/themes/default/theme.css b/admin/themes/default/theme.css index a8b075766..d7d3e96ca 100644 --- a/admin/themes/default/theme.css +++ b/admin/themes/default/theme.css @@ -8454,7 +8454,7 @@ color:#FF7B00; /* Filters options */ .filters-grid{ display: grid; - grid-template-columns: 200px 118px 25px; + grid-template-columns: 200px 150px 25px; margin-bottom: 0px !important; } .select-views{ @@ -8468,7 +8468,7 @@ color:#FF7B00; .select-views-arrow{ display: flex; position: absolute; - margin-left: 299.9px; + margin-left: 333px; margin-top: 5px; pointer-events: none; } diff --git a/include/config_default.inc.php b/include/config_default.inc.php index 0dd0a84d1..4dc7fec47 100644 --- a/include/config_default.inc.php +++ b/include/config_default.inc.php @@ -1054,6 +1054,7 @@ $conf['default_filters_views'] = array( 'file_size' => ['access'=>'everybody', 'default'=>false], 'height' => ['access'=>'everybody', 'default'=>false], 'width' => ['access'=>'everybody', 'default'=>false], + 'expert' => ['access'=>'everybody', 'default'=>false], 'last_filters_conf' => true, ); diff --git a/include/functions_search.inc.php b/include/functions_search.inc.php index 8fda28f6c..1df62e96d 100644 --- a/include/functions_search.inc.php +++ b/include/functions_search.inc.php @@ -119,15 +119,7 @@ function get_regular_search_results($search, $images_where='') $image_ids_for_filter = array(); - if (isset($conf['filters_views'])) - { - $display_filters = unserialize($conf['filters_views']); - } - else - { - $display_filters = unserialize('a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}'); - } - + $display_filters = safe_unserialize(conf_get_param('filters_views', $conf['default_filters_views'])); foreach($display_filters as $filt_name => $filt_conf){ if(isset($filt_conf['access'])) @@ -146,7 +138,7 @@ function get_regular_search_results($search, $images_where='') // // expert // - if (isset($search['fields']['expert']) and !empty($search['fields']['expert']['string'])) + if (isset($search['fields']['expert']) and !empty($search['fields']['expert']['string']) and $display_filters['expert']['access']) { $has_filters_filled = true; diff --git a/include/search_filters.inc.php b/include/search_filters.inc.php index 7ffb65636..11ec3198a 100644 --- a/include/search_filters.inc.php +++ b/include/search_filters.inc.php @@ -109,7 +109,14 @@ if ('search' == $page['section'] and isset($page['search_details'])) if (isset($my_search['fields']['expert'])) { - load_language('help_quick_search.lang'); + if (!$display_filters['expert']['access']) + { + unset($my_search['fields']['expert']); + } + else + { + load_language('help_quick_search.lang'); + } } if (isset($my_search['fields']['author']) and $display_filters['author']['access']) diff --git a/install/db/178-database.php b/install/db/178-database.php index 29c32da7a..ef8365f7f 100644 --- a/install/db/178-database.php +++ b/install/db/178-database.php @@ -13,7 +13,9 @@ if (!defined('PHPWG_ROOT_PATH')) $upgrade_description = 'add config parameters to the gallery filters'; -conf_update_param('filters_views', $conf['default_filters_views']); +// let the $conf['filters_views'] be written in config table when the admin will change settings in administration. +// +// conf_update_param('filters_views', $conf['default_filters_views']); echo "\n".$upgrade_description."\n"; diff --git a/install/db/181-database.php b/install/db/181-database.php new file mode 100644 index 000000000..f0b2d9bd8 --- /dev/null +++ b/install/db/181-database.php @@ -0,0 +1,33 @@ + diff --git a/language/en_UK/admin.lang.php b/language/en_UK/admin.lang.php index b5e93b9ee..f108c3a60 100644 --- a/language/en_UK/admin.lang.php +++ b/language/en_UK/admin.lang.php @@ -1420,5 +1420,6 @@ $lang['If a photo in this album has the same filename, update the file without c $lang['Empty lounge'] = 'Empty lounge'; $lang['There is currently %d photos in the lounge (upload buffer)'] = 'There is currently %d photos in the lounge (upload buffer)'; $lang['%d photos were moved from the upload lounge to their albums'] = '%d photos were moved from the upload lounge to their albums'; +$lang['Admins only'] = 'Admins only'; // Leave this line empty \ No newline at end of file diff --git a/language/fr_FR/admin.lang.php b/language/fr_FR/admin.lang.php index a1fd32f4e..e7613c735 100644 --- a/language/fr_FR/admin.lang.php +++ b/language/fr_FR/admin.lang.php @@ -1422,4 +1422,5 @@ $lang['If a photo in this album has the same filename, update the file without c $lang['Empty lounge'] = 'Vider le lounge'; $lang['There is currently %d photos in the lounge (upload buffer)'] = 'Il y a actuellement %d photos dans le lounge (salle d\'attente des transferts)'; $lang['%d photos were moved from the upload lounge to their albums'] = '%d photos ont été déplacées du lounge vers leurs albums respectifs'; +$lang['Admins only'] = 'Admins uniquement'; // Leave this line empty diff --git a/search.php b/search.php index c7391b3f7..5ddafe471 100644 --- a/search.php +++ b/search.php @@ -28,38 +28,28 @@ $search = array( ); // list of filters in user preferences -// allwords, cat, tags, author, added_by, filetypes, date_posted, date_created, ratios, ratings (if rating is allowed in this Piwigo), height, width -//import the conf for the filters -if (isset($conf['filters_views'])) -{ - $filters_conf = unserialize($conf['filters_views']); -} -else -{ - $filters_conf = unserialize('a:14:{s:5:"words";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:4:"tags";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"post_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:13:"creation_date";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:5:"album";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:1;}s:6:"author";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:8:"added_by";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_type";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"ratio";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"rating";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:9:"file_size";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:6:"height";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:5:"width";a:2:{s:6:"access";s:9:"everybody";s:7:"default";b:0;}s:17:"last_filters_conf";b:1;}'); -} +$filters_views = safe_unserialize(conf_get_param('filters_views', $conf['default_filters_views'])); //change the name of the keys so that they can be used with this part of the program -$filters_conf = array_combine -( - array('allwords', - 'tags', - 'date_posted', - 'date_created', - 'cat', - 'author', - 'added_by', - 'filetypes', - 'ratios', - 'ratings', - 'filesize', - 'height', - 'width', - 'last_filters_conf' - ), - $filters_conf +$filter_rename_for = array( + 'words' => 'allwords', + 'post_date' => 'date_posted', + 'creation_date' => 'date_created', + 'album' => 'cat', + 'file_type' => 'filetypes', + 'ratio' => 'ratios', + 'rating' => 'ratings', + 'file_size' => 'filesize', ); +$filters_conf = array(); +foreach ($filters_views as $filter_name => $filter_value) +{ + $key = isset($filter_rename_for[$filter_name]) ? $filter_rename_for[$filter_name] : $filter_name; + + $filters_conf[$key] = $filter_value; +} + //get all default filters $default_fields = array(); foreach($filters_conf as $filt_name => $filt_conf){ diff --git a/themes/default/template/include/search_filters.inc.tpl b/themes/default/template/include/search_filters.inc.tpl index 0e3284e11..2b9091d69 100644 --- a/themes/default/template/include/search_filters.inc.tpl +++ b/themes/default/template/include/search_filters.inc.tpl @@ -191,11 +191,12 @@ const prefix_icon = 'gallery-icon-'; {/if} +{if $display_filter.expert.access == 'everybody' or ($display_filter.expert.access == 'admins-only' and is_admin()) or ($display_filter.expert.access == 'registered-users' and is_classic_user())} - +{/if}
From d6a1cf0466c69609e55b939b8e45b47c3f95f968 Mon Sep 17 00:00:00 2001 From: Linty Date: Wed, 29 Oct 2025 11:49:19 +0100 Subject: [PATCH 059/149] fixes #2424 remove connection by header from API key validation Simplifies the API key validation in auth_key_login by removing the requirement for connection_by_header. Now, API keys matching the pattern are accepted regardless of the connection source. --- include/functions_user.inc.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/include/functions_user.inc.php b/include/functions_user.inc.php index b52645b8e..29a0fdf68 100644 --- a/include/functions_user.inc.php +++ b/include/functions_user.inc.php @@ -1677,10 +1677,7 @@ function auth_key_login($auth_key, $connection_by_header=false) { $valid_key = 'auth_key'; } - else if ( - preg_match('/^pkid-\d{8}-[a-z0-9]{20}:[a-z0-9]{40}$/i', $auth_key) - and $connection_by_header - ) + else if (preg_match('/^pkid-\d{8}-[a-z0-9]{20}:[a-z0-9]{40}$/i', $auth_key)) { $valid_key = 'api_key'; $tmp_key = explode(':', $auth_key); From 99bb370b98dd79cb8e37fc4080bfa08e5bad3249 Mon Sep 17 00:00:00 2001 From: Linty Date: Wed, 29 Oct 2025 12:03:43 +0100 Subject: [PATCH 060/149] fixes #2425 unescape API key name before returning Added a call to stripslashes for the 'apikey_name' field to ensure it is unescaped before being returned. This improves display consistency for API key names containing escaped characters. --- include/functions_user.inc.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/functions_user.inc.php b/include/functions_user.inc.php index 29a0fdf68..82ffcf52e 100644 --- a/include/functions_user.inc.php +++ b/include/functions_user.inc.php @@ -2608,6 +2608,8 @@ SELECT $api_key['apikey_secret'] = str_repeat("*", 40); unset($api_key['auth_key_id'], $api_key['user_id'], $api_key['key_type']); + $api_key['apikey_name'] = stripslashes($api_key['apikey_name']); + $api_key['created_on_format'] = format_date($api_key['created_on'], array('day', 'month', 'year')); $api_key['expired_on_format'] = format_date($api_key['expired_on'], array('day', 'month', 'year')); $api_key['last_used_on_since'] = From d600b019a6da53147d56671100e8363fc0c97179 Mon Sep 17 00:00:00 2001 From: Linty Date: Wed, 29 Oct 2025 12:23:54 +0100 Subject: [PATCH 061/149] enable qrcode generation for totp secrets Uncommented and activated QR code generation in getQrCode(), allowing TOTP secrets to be encoded as base64 PNG images for easier setup in authenticator apps. --- include/totp.class.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/totp.class.php b/include/totp.class.php index fcab5a5a1..cab17128e 100644 --- a/include/totp.class.php +++ b/include/totp.class.php @@ -2,6 +2,7 @@ defined('PHPWG_ROOT_PATH') or die('Hacking attempt!'); require_once(PHPWG_ROOT_PATH . 'include/base32.class.php'); +require_once(PHPWG_ROOT_PATH . 'include/phpqrcode.php'); class PwgTOTP { @@ -62,14 +63,13 @@ class PwgTOTP */ public static function getQrCode($secret) { - // require_once(TF_REALPATH . 'include/phpqrcode.php'); - // $otp_url = self::getOtpAuthUrl($secret); + $otp_url = self::getOtpAuthUrl($secret); - // ob_start(); - // QRcode::png($otp_url); - // $qrcode_image = ob_get_clean(); - // $base64_qrcode = base64_encode($qrcode_image); - // return 'data:image/png;base64,' . $base64_qrcode; + ob_start(); + QRcode::png($otp_url); + $qrcode_image = ob_get_clean(); + $base64_qrcode = base64_encode($qrcode_image); + return 'data:image/png;base64,' . $base64_qrcode; } /** From 696236e76bdd1bcaa6709447031c12083338e8b0 Mon Sep 17 00:00:00 2001 From: Linty Date: Wed, 29 Oct 2025 13:00:24 +0100 Subject: [PATCH 062/149] fixes #2426 move authorization section into test section Moved API key authentication input from a separate card to the test form section for improved clarity. Updated related CSS for better layout and consistency, and adjusted descriptions to highlight API key usage in Piwigo 16. --- tools/ws.htm | 26 +++++++++++--------------- tools/ws/ws.css | 13 +++++++------ 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/tools/ws.htm b/tools/ws.htm index 296f471c7..7c52683a9 100644 --- a/tools/ws.htm +++ b/tools/ws.htm @@ -75,6 +75,11 @@ This page lists all API methods available on your Piwigo installation, part of the Piwigo core or added by third-party plugins. For each method you can consult required and optional parameters, and even test them in direct live!

+ +

Introduced in Piwigo 16, you can now use an API key in the HTTP header + to perform authenticated requests without a user session. + For more details, check out our documentation. +

For more information you can consult our Wiki Piwigo Web API and our forums. @@ -84,21 +89,6 @@