From 8b5fbd6724079aad6d87e3e39e5ab9dbbc2b488d Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:19:28 +0300 Subject: [PATCH 01/27] wip duplication --- includes/controllers/ApiController.php | 45 +++- includes/controllers/PageController.php | 9 + includes/services/PageManager.php | 7 + tools/bazar/services/EntryManager.php | 8 + tools/templates/actions/barreredaction.php | 55 +++-- tools/templates/lang/templates_fr.inc.php | 7 +- .../templates/barreredaction_basic.twig | 233 +++++++++++------- 7 files changed, 239 insertions(+), 125 deletions(-) diff --git a/includes/controllers/ApiController.php b/includes/controllers/ApiController.php index d13aa97de..77cef95cd 100644 --- a/includes/controllers/ApiController.php +++ b/includes/controllers/ApiController.php @@ -44,9 +44,15 @@ public function getDocumentation() $urlPages = $this->wiki->Href('', 'api/pages'); $output .= '

' . _t('PAGES') . '

' . "\n" . - '

GET ' . $urlPages . '

'; - $urlPagesComments = $this->wiki->Href('', 'api/pages/{pageTag}/comments'); - $output .= '

GET ' . $urlPagesComments . '

'; + '

GET ' . $urlPages . '
Get all pages

'; + $urlPages = $this->wiki->Href('', 'api/pages/{pageTag}'); + $output .= '

GET ' . $urlPages . '
Get indicated page\'s informations, with raw and html contents

'; + + $urlPages = $this->wiki->Href('', 'api/pages/{pageTag}/comments'); + $output .= '

GET ' . $urlPages . '
Get indicated page\'s comments

'; + + $urlPages = $this->wiki->Href('', 'api/pages/{pageTag}/duplicate'); + $output .= '

GET ' . $urlPages . '
Duplicate page into same YesWiki (in edit mode with edit=1 param) or to another YesWiki (with toUrl=1 param)

'; $urlComments = $this->wiki->Href('', 'api/comments'); $output .= '

' . _t('COMMENTS') . '

' . "\n" . @@ -71,7 +77,7 @@ public function getDocumentation() if (file_exists($pluginBase . 'controllers/ApiController.php')) { $apiClassName = 'YesWiki\\' . ucfirst($extension) . '\\Controller\\ApiController'; if (!class_exists($apiClassName, false)) { - include $pluginBase . 'controllers/ApiController.php'; + include($pluginBase . 'controllers/ApiController.php'); } if (class_exists($apiClassName, false)) { $apiController = new $apiClassName(); @@ -382,6 +388,27 @@ public function getPage(Request $request, $tag) return new ApiResponse($page); } + /** + * @Route("/api/pages/{tag}/duplicate",methods={"GET"},options={"acl":{"public"}}) + */ + public function duplicatePage(Request $request, $tag) + { + $this->denyAccessUnlessGranted('read', $tag); + $pageManager = $this->getService(PageManager::class); + $page = $pageManager->getOne($tag, $request->get('time')); + if (!$page) { + return new ApiResponse(null, Response::HTTP_NOT_FOUND); + } + + if (!empty($request->get('destination'))) { + return new ApiResponse('Tag de destination : ' . $request->get('destination')); + } else { + return new ApiResponse('Tag de destination ?'); + } + $pageManager = $this->getService(PageManager::class); + $entryManager = $this->getService(EntryManager::class); + } + /** * @Route("/api/pages/{tag}",methods={"DELETE"},options={"acl":{"public","+"}}) */ @@ -820,11 +847,14 @@ private function extractTriplesParams(string $method, $resource): array Response::HTTP_BAD_REQUEST ); } else { - $property = $this->getService(SecurityController::class)->filterInput($method, 'property', FILTER_DEFAULT, true); + $property = filter_input($method, 'property', FILTER_UNSAFE_RAW); + $property = in_array($property, [false, null], true) ? "" : htmlspecialchars(strip_tags($property)); if (empty($property)) { $property = null; } - $username = $this->getService(SecurityController::class)->filterInput($method, 'user', FILTER_DEFAULT, true); + + $username = filter_input($method, 'user', FILTER_UNSAFE_RAW); + $username = in_array($username, [false, null], true) ? "" : htmlspecialchars(strip_tags($username)); if (empty($username)) { if (!$this->wiki->UserIsAdmin()) { $username = $this->getService(AuthController::class)->getLoggedUser()['name']; @@ -840,7 +870,6 @@ private function extractTriplesParams(string $method, $resource): array ); } } - return compact(['property', 'username', 'apiResponse']); } @@ -859,7 +888,7 @@ public function getArchiveStatus($uid) { return $this->getService(ArchiveController::class)->getArchiveStatus( $uid, - empty($_GET['forceStarted']) ? false : in_array($_GET['forceStarted'], [1, true, '1', 'true'], true) + empty($_GET['forceStarted']) ? false : in_array($_GET['forceStarted'], [1, true, "1", "true"], true) ); } diff --git a/includes/controllers/PageController.php b/includes/controllers/PageController.php index 08e954018..26f4194cc 100644 --- a/includes/controllers/PageController.php +++ b/includes/controllers/PageController.php @@ -46,4 +46,13 @@ public function delete(string $tag): bool return true; } } + + public function duplicate(string $sourceTag, string $destinationTag = '') : bool + { + if ($this->entryManager->isEntry($sourceTag)){ + return $this->entryController->duplicate($sourceTag, $destinationTag); + } else { + return $this->pageManager->duplicate($sourceTag, $destinationTag); + } + } } diff --git a/includes/services/PageManager.php b/includes/services/PageManager.php index 59f0c347a..fd340af47 100644 --- a/includes/services/PageManager.php +++ b/includes/services/PageManager.php @@ -506,4 +506,11 @@ private function checkEntriesACL(array $pages, ?string $tag = null, ?string $use return $pages; } + + private function duplicate($sourceTag, $destinationTag) : boolean + { + $result = false; + $this->wiki->LogAdministrativeAction($this->authController->getLoggedUserName(), "Duplication de la page \"\"" . $sourceTag . "\"\" vers la page \"\"".$destinationTag."\"\""); + return $result; + } } diff --git a/tools/bazar/services/EntryManager.php b/tools/bazar/services/EntryManager.php index b6b4330a8..2612688c7 100644 --- a/tools/bazar/services/EntryManager.php +++ b/tools/bazar/services/EntryManager.php @@ -1180,4 +1180,12 @@ function ($attributeName) { return $entriesIds; } + + private function duplicate($sourceTag, $destinationTag) : boolean + { + $result = false; + $this->wiki->LogAdministrativeAction($this->authController->getLoggedUserName(), "Duplication de la fiche \"\"" . $sourceTag . "\"\" vers la fiche \"\"".$destinationTag."\"\""); + return $result; + } + } diff --git a/tools/templates/actions/barreredaction.php b/tools/templates/actions/barreredaction.php index a77979d02..fd7a68fb7 100755 --- a/tools/templates/actions/barreredaction.php +++ b/tools/templates/actions/barreredaction.php @@ -21,8 +21,8 @@ $content = $this->LoadPage($page); $time = $content['time']; } - $barreredactionelements['page'] = $page; - $barreredactionelements['linkpage'] = $this->href('', $page); + $options['page'] = $page; + $options['linkpage'] = $this->href('', $page); // on choisit le template utilisé $template = $this->GetParameter('template'); @@ -30,20 +30,20 @@ $template = 'barreredaction_basic.twig'; } - // on peut ajouter des classes - $barreredactionelements['class'] = $this->GetParameter('class') || ''; + // on peut ajouter des classes, la classe par défaut est .footer + $options['class'] = ($this->GetParameter('class') ? 'footer ' . $this->GetParameter('class') : 'footer'); if ($this->HasAccess('write')) { // on ajoute le lien d'édition si l'action est autorisée - if ($this->HasAccess('write', $page) && !$this->services->get(SecurityController::class)->isWikiHibernated()) { - $barreredactionelements['linkedit'] = $this->href('edit', $page); + if ($this->HasAccess("write", $page) && !$this->services->get(SecurityController::class)->isWikiHibernated()) { + $options['linkedit'] = $this->href("edit", $page); } if ($time) { // hack to hide E_STRICT error if no timezone set date_default_timezone_set(@date_default_timezone_get()); - $barreredactionelements['linkrevisions'] = $this->href('revisions', $page); - $barreredactionelements['time'] = date(_t('TEMPLATE_DATE_FORMAT'), strtotime($time)); + $options['linkrevisions'] = $this->href("revisions", $page); + $options['time'] = date(_t('TEMPLATE_DATE_FORMAT'), strtotime($time)); } // if this page exists @@ -51,48 +51,49 @@ $owner = $this->GetPageOwner($page); // message if ($this->UserIsOwner($page)) { - $barreredactionelements['owner'] = _t('TEMPLATE_OWNER') . ' : ' . _t('TEMPLATE_YOU'); + $options['owner'] = _t('TEMPLATE_OWNER') . " : " . _t('TEMPLATE_YOU'); } elseif ($owner) { - $barreredactionelements['owner'] = _t('TEMPLATE_OWNER') . ' : ' . $owner; + $options['owner'] = _t('TEMPLATE_OWNER') . " : " . $owner; } else { - $barreredactionelements['owner'] = _t('TEMPLATE_NO_OWNER'); + $options['owner'] = _t('TEMPLATE_NO_OWNER'); } // if current user is owner or admin if ($this->UserIsOwner($page) || $this->UserIsAdmin()) { - $barreredactionelements['owner'] .= ' - ' . _t('TEMPLATE_PERMISSIONS'); + $options['owner'] .= ' - ' . _t('TEMPLATE_PERMISSIONS'); if (!$this->services->get(SecurityController::class)->isWikiHibernated()) { - $barreredactionelements['linkacls'] = $this->href('acls', $page); - $barreredactionelements['linkdeletepage'] = $this->href('deletepage', $page); + $options['linkacls'] = $this->href("acls", $page); + $options['linkdeletepage'] = $this->href("deletepage", $page); } $aclsService = $this->services->get(AclService::class); $hasAccessComment = $aclsService->hasAccess('comment'); - $barreredactionelements['wikigroups'] = $this->GetGroupsList(); + $options['wikigroups'] = $this->GetGroupsList(); if ($this->services->get(ParameterBagInterface::class)->get('comments_activated')) { if ($hasAccessComment && $hasAccessComment !== 'comments-closed') { - $barreredactionelements['linkclosecomments'] = $this->href('claim', $page, ['action' => 'closecomments'], false); + $options['linkclosecomments'] = $this->href("claim", $page, ['action' => 'closecomments'], false); } else { - $barreredactionelements['linkopencomments'] = $this->href('claim', $page, ['action' => 'opencomments'], false); + $options['linkopencomments'] = $this->href("claim", $page, ['action' => 'opencomments'], false); } } } elseif (!$owner && $this->GetUser()) { - $barreredactionelements['owner'] .= ' - ' . _t('TEMPLATE_CLAIM'); + $options['owner'] .= " - " . _t('TEMPLATE_CLAIM'); if (!$this->services->get(SecurityController::class)->isWikiHibernated()) { - $barreredactionelements['linkacls'] = $this->href('claim', $page); + $options['linkacls'] = $this->href("claim", $page); } } } } - $barreredactionelements['linkshare'] = $this->href('share', $page); - $barreredactionelements['userIsOwner'] = $this->UserIsOwner($page); - $barreredactionelements['userIsAdmin'] = $this->UserIsAdmin(); - $barreredactionelements['userIsAdminOrOwner'] = $this->UserIsAdmin() || $this->UserIsOwner($page); + $options['linkduplicate'] = $this->href('', 'api/' . $page . '/duplicate'); + $options['linkshare'] = $this->href("share", $page); + $options['userIsOwner'] = $this->UserIsOwner($page); + $options['userIsAdmin'] = $this->UserIsAdmin(); + $options['userIsAdminOrOwner'] = $this->UserIsAdmin() || $this->UserIsOwner($page); $favoritesManager = $this->services->get(FavoritesManager::class); if (!empty($user) && $favoritesManager->areFavoritesActivated()) { - $barreredactionelements['currentuser'] = $user['name']; - $barreredactionelements['isUserFavorite'] = $favoritesManager->isUserFavorite($user['name'], $page); + $options['currentuser'] = $user['name']; + $options['isUserFavorite'] = $favoritesManager->isUserFavorite($user['name'], $page); } - echo $this->render("@templates/$template", $barreredactionelements); + echo $this->render("@templates/$template", $options); echo ' ' . "\n"; -} \ No newline at end of file +} diff --git a/tools/templates/lang/templates_fr.inc.php b/tools/templates/lang/templates_fr.inc.php index 46553cba3..eba03eab4 100644 --- a/tools/templates/lang/templates_fr.inc.php +++ b/tools/templates/lang/templates_fr.inc.php @@ -49,7 +49,12 @@ 'TEMPLATE_CLOSE_COMMENTS' => 'Fermer les commentaires', 'TEMPLATE_FOR_CONNECTED_PEOPLE' => 'Pour les personnes connectées', 'TEMPLATE_FOR_MEMBERS_OF_GROUP' => 'Pour les membres du groupe', - 'TEMPLATES_SEE_ATTACHED_FILES' => 'Voir les fichiers attachés à la page', + 'TEMPLATE_SEE_ATTACHED_FILES' => 'Voir les fichiers attachés à la page', + 'TEMPLATE_DUPLICATE' => 'Dupliquer', + 'TEMPLATE_DUPLICATE_AND_SEE' => 'Dupliquer localement et voir la page dupliquée', + 'TEMPLATE_DUPLICATE_AND_EDIT' => 'Dupliquer localement et éditer la page dupliquée', + 'TEMPLATE_DUPLICATE_IN_ANOTHER_WIKI' => 'Dupliquer dans un autre YesWiki', + // action/diaporama 'DIAPORAMA_PAGE_PARAM_MISSING' => 'Action diaporama : paramêtre "page" obligatoire.', 'DIAPORAMA_TEMPLATE_PARAM_ERROR' => 'Action diaporama : le paramêtre "template" pointe sur un fichier inexistant ou illisible. Le template par défaut sera utilisé.', diff --git a/tools/templates/templates/barreredaction_basic.twig b/tools/templates/templates/barreredaction_basic.twig index 9750869a9..4324b5616 100644 --- a/tools/templates/templates/barreredaction_basic.twig +++ b/tools/templates/templates/barreredaction_basic.twig @@ -1,89 +1,144 @@ - +
+ {% if linkedit is defined %} + + {{ _t('TEMPLATE_EDIT_THIS_PAGE') }} + + {% endif %} + {% if userIsAdminOrOwner %} + + + + {% endif %} + + + + + {% if linkdeletepage is defined %} + + {{ _t('TEMPLATE_DELETE') }} + + {% endif %} + {% if time is defined %} + + {{ _t('TEMPLATE_LAST_UPDATE') }} {{ time }} + + {% endif %} + {% if linkacls is defined %} + + {{ owner ?? ''}} + + {% endif %} + {% if linkclosecomments is defined %} + + {{ _t('TEMPLATE_CLOSE_COMMENTS') }} + + {% endif %} + {% if linkopencomments is defined %} + + + + + {% endif %} + + {{ _t('TEMPLATE_SHARE') }} + + {% if currentuser is not empty %} + {{ include_javascript('javascripts/favorites.js') }} + + + {{ (isUserFavorite) ? _t('FAVORITES_REMOVE') : _t('FAVORITES_ADD') }} + + {% endif %} +
From e65ca0b5bbea8bf7041e5765a31b5eff28878177 Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 02/27] feat(duplication) : wip duplication handler --- handlers/DuplicateHandler.php | 69 +++++ includes/services/ImportFilesManager.php | 277 ++++++++++++++++++ includes/services/PageManager.php | 14 +- lang/yeswiki_fr.php | 9 +- templates/handlers/duplicate-inner.twig | 16 + templates/handlers/duplicate.twig | 4 + tools/bazar/services/EntryManager.php | 5 +- tools/templates/actions/barreredaction.php | 2 +- tools/templates/lang/templates_fr.inc.php | 4 +- .../templates/barreredaction_basic.twig | 18 +- 10 files changed, 392 insertions(+), 26 deletions(-) create mode 100644 handlers/DuplicateHandler.php create mode 100644 includes/services/ImportFilesManager.php create mode 100644 templates/handlers/duplicate-inner.twig create mode 100644 templates/handlers/duplicate.twig diff --git a/handlers/DuplicateHandler.php b/handlers/DuplicateHandler.php new file mode 100644 index 000000000..581166f4b --- /dev/null +++ b/handlers/DuplicateHandler.php @@ -0,0 +1,69 @@ +authController = $this->getService(AuthController::class); + $this->entryController = $this->getService(EntryController::class); + $this->importManager = $this->getService(ImportFilesManager::class); + $output = $title = ''; + if (!$this->wiki->page) { + $output = $this->render('@templates\alert-message.twig', [ + 'type' => 'info', + 'message' => str_replace( + ["{beginLink}", "{endLink}"], + ["wiki->href('edit')}\">", ""], + _t("NOT_FOUND_PAGE") + ), + ]); + } elseif ($this->getService(AclService::class)->hasAccess('read', $this->wiki->GetPageTag())) { + $title = _t('TEMPLATE_DUPLICATE_PAGE') . ' ' . $this->wiki->GetPageTag(); + $attachments = $this->importManager->findDirectLinkAttachements($this->wiki->page['tag']); + $totalSize = 0; + foreach ($attachments as $a) { + $totalSize = $totalSize + $a['size']; + } + $output .= $this->render('@core/handlers/duplicate-inner.twig', [ + 'attachments' => $attachments, + 'totalSize' => $this->importManager->humanFilesize($totalSize), + 'isEntry' => $this->getService(EntryManager::class)->isEntry($this->wiki->GetPageTag()), + 'toExternalWiki' => isset($_GET['toUrl']) && $_GET['toUrl'] == "1" + ]); + } else { // if no read access to the page + if ($contenu = $this->getService(PageManager::class)->getOne("PageLogin")) { + // si une page PageLogin existe, on l'affiche + $output .= $this->wiki->Format($contenu["body"]); + } else { + // sinon on affiche le formulaire d'identification minimal + $output .= '
' . "\n" + . '
' . "\n" + . _t('LOGIN_NOT_AUTORIZED') . '. ' . _t('LOGIN_PLEASE_REGISTER') . '.' . "\n" + . '
' . "\n" + . $this->wiki->Format('{{login signupurl="0"}}' . "\n\n") + . '
' . "\n"; + } + } + // in ajax request for modal, no title + if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { + $title = ''; + } + return $this->renderInSquelette('@core/handlers/duplicate.twig', [ + 'title' => $title, + 'output' => $output, + ]); + } +} diff --git a/includes/services/ImportFilesManager.php b/includes/services/ImportFilesManager.php new file mode 100644 index 000000000..7095ee2b5 --- /dev/null +++ b/includes/services/ImportFilesManager.php @@ -0,0 +1,277 @@ +wiki = $wiki; + $this->uploadPath = null; + } + + /** + * Get the local path to files uploads (usually "files") + * + * @return string local path to files uploads + */ + private function getLocalFileUploadPath() + { + if ($this->uploadPath !== null) { + return $this->uploadPath; + } + + $attachConfig = $this->wiki->config['attach_config']; + + if (!is_array($attachConfig)) { + $attachConfig = array(); + } + + if (empty($attachConfig['upload_path'])) { + $this->uploadPath = 'files'; + } else { + $this->uploadPath = $attachConfig['upload_path']; + } + + return $this->uploadPath; + } + + /** + * Download file url to local wiki using cURL + * + * @param string $from file url + * @param string $to local path + * @param boolean $overwrite overwrite existing file ? (default:false) + * @return void + */ + private function cURLDownload($from, $to, $overwrite = false) + { + $output = ''; + if (file_exists($to)) { + if ($overwrite) { + $output .= _t('FILE') . ' ' . $to . ' ' . _t('FILE_OVERWRITE') . '.'; + } else { + $output .= _t('FILE') . ' ' . $to . ' ' . _t('FILE_NO_OVERWRITE') . '.'; + return $output; + } + } + + // Do cURL transfer + $fp = fopen($to, 'wb'); + $ch = curl_init($from); + curl_setopt($ch, CURLOPT_FILE, $fp); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_FAILONERROR, true); + curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + fclose($fp); + + if ($err) { + unlink($to); + throw new \Exception($output . _t('ERROR_DOWNLOADING') . ' ' . $from . ': ' . $err . "\n" . _t('REMOVING_CORRUPTED_FILE') . ' ' . $to); + } + return $output; + } + + /** + * Return fields that may contain attachments to import (body for wikipage, or textelong fields for bazar entries) + * + * @param array $wikiPage page or entry content as an array + * @return array keys of $wikiPage that may contain attachments to import + */ + public function getTextFieldsFromWikiPage($wikiPage) + { + $fields = []; + if (!empty($wikiPage['tag'])) { // classic wiki page + $fields[] = 'body'; + } elseif (!empty($wikiPage['id_fiche'])) { // bazar entry + $formManager = $this->wiki->services->get(FormManager::class); + $form = $formManager->getOne($wikiPage['id_typeannonce']); + // find fields that are textareas + foreach ($form['prepared'] as $field) { + if ($field instanceof TextareaField) { + $fields[] = $field->getName(); + } + } + } + return $fields; + } + + /** + * Get attachements from raw page content + * + * @param string $tag page id + * @return array attachments filenames + */ + public function findDirectLinkAttachements($tag = '') + { + if (empty(trim($tag))) { + $tag = $this->wiki->GetPageTag(); + } + $rawContent = $this->wiki->services->get(PageManager::class)->getOne($tag)['body']; + $regex = '#\{\{attach.*file="(.*)".*\}\}#Ui'; + preg_match_all( + $regex, + $rawContent, + $attachments + ); + if (is_array($attachments[1])) { + $filesMatched = []; + foreach ($attachments[1] as $a) { + $ext = pathinfo($a, PATHINFO_EXTENSION); + $filename = pathinfo($a, PATHINFO_FILENAME); + $searchPattern = '`^' . $tag . '_' . $filename . '_\d{14}_\d{14}\.' . $ext . '_?$`'; + $path = $this->getLocalFileUploadPath(); + $fh = opendir($path); + while (($file = readdir($fh)) !== false) { + if (strcmp($file, '.') == 0 || strcmp($file, '..') == 0 || is_dir($file)) { + continue; + } + if (preg_match($searchPattern, $file)) { + $filePath = $path . '/' . $file; + $size = filesize($filePath); + $humanSize = $this->humanFilesize($size); + $filesMatched[] = ['path' => $filePath, 'size' => $size, 'humanSize' => $humanSize]; + } + } + } + } + return $filesMatched; + } + + public function humanFilesize($bytes, $decimals = 2) + { + $factor = floor((strlen($bytes) - 1) / 3); + if ($factor > 0) { + $sz = 'KMGT'; + } + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor - 1] . 'B'; + } + + /** + * Generate distant file url and download to local file path + * + * @param string $remoteUrl distant file url + * @param string $filename file name + * @param boolean $overwrite overwrite existing file ? (default:false) + * @return void + */ + public function downloadDirectLinkAttachment($remoteUrl, $filename, $overwrite = false) + { + $remoteFileUrl = $remoteUrl . '/files/' . $filename; + $saveFileLoc = $this->getLocalFileUploadPath() . '/' . $filename; + + return $this->cURLDownload($remoteFileUrl, $saveFileLoc, $overwrite); + } + + /** + * Find file attachments in page or bazar entry + * It finds attachments linked with /download links + * + * @param string $remoteUrl distant url + * @param array $wikiPage page or entry content as an array + * @param boolean $transform transform attachments urls for their new location (default:false) + * @return array all file attachments + */ + public function findHiddenAttachments($remoteUrl, &$wikiPage, $transform = false) + { + preg_match_all( + '#(?:href|src)="' . preg_quote($remoteUrl, '#') . '\?.+/download&(?:amp;)?file=(?P.*)"#Ui', + $wikiPage['html_output'], + $htmlMatches + ); + $attachments = $htmlMatches['filename']; + + $wikiRegex = '#="' . preg_quote($remoteUrl, '#') + . '(?P\?.+/download&(?:amp;)?file=(?P.*))"#Ui'; + + $contentKeys = $this->getTextFieldsFromWikiPage($wikiPage); + foreach ($contentKeys as $key) { + preg_match_all($wikiRegex, $wikiPage[$key], $wikiMatches); + $attachments = array_merge($attachments, $wikiMatches['filename']); + } + + $attachments = array_unique($attachments); + + if ($transform) { + foreach ($contentKeys as $key) { + $wikiPage[$key] = preg_replace($wikiRegex, '="' . $this->wiki->getBaseUrl() . '${trail}"', $wikiPage[$key]); + } + } + + return $attachments; + } + + /** + * Generate local path and download hidden attachments + * It downloads attachments linked with /download links + * + * @param string $remoteUrl distant url + * @param string $pageTag page tag + * @param string $lastPageUpdate last update time + * @param string $filename file name + * @param boolean $overwrite overwrite existing file ? (default:false) + * @return array all file attachments + */ + public function downloadHiddenAttachment($remoteUrl, $pageTag, $lastPageUpdate, $filename, $overwrite = false) + { + if (!class_exists('attach')) { + require_once("tools/attach/libs/attach.lib.php"); + } + + $this->wiki->tag = $pageTag; + $this->wiki->page = array('tag' => $pageTag, 'time' => $lastPageUpdate); + + $remoteFileUrl = $remoteUrl . '?' . $pageTag . '/download&file=' . $filename; + $att = new \attach($this->wiki); + $att->file = $filename; + $newFilename = $att->GetFullFilename(true); + + $this->cURLDownload($remoteFileUrl, $newFilename, $overwrite); + } + + + /** + * All type of attachment related to a page or a bazar entry + * + * @param string $remoteUrl distant url + * @param array $wikiPage page or entry content as an array + * @param boolean $overwrite overwrite existing file ? (default:false) + * @return void + */ + public function downloadAttachments($remoteUrl, &$wikiPage, $overwrite = false) + { + // Handle Pictures and file attachments + $attachments = $this->findDirectLinkAttachements($remoteUrl, $wikiPage, true); + + if (count($attachments)) { + foreach ($attachments as $image) { + $this->downloadDirectLinkAttachment($remoteUrl, $image, $overwrite); + } + } + + // Downloading hidden attachments + $attachments = $this->findHiddenAttachments($remoteUrl, $wikiPage, true); + + if (!empty($attachments)) { + foreach ($attachments as $attachment) { + $this->downloadHiddenAttachment($remoteUrl, $wikiPage['id_fiche'], date("Y-m-d H:i:s"), $attachment, $overwrite); + } + } + } +} diff --git a/includes/services/PageManager.php b/includes/services/PageManager.php index fd340af47..678a8a659 100644 --- a/includes/services/PageManager.php +++ b/includes/services/PageManager.php @@ -219,7 +219,6 @@ public function getAll(): array SELECT * FROM {$this->dbService->prefixTable('pages')} WHERE LATEST = 'Y' ORDER BY tag SQL); $pages = $this->checkEntriesACL($pages); - return $pages; } @@ -298,6 +297,7 @@ public function deleteOrphaned($tag) $this->dbService->query("DELETE FROM {$this->dbService->prefixTable('links')} WHERE from_tag='{$this->dbService->escape($tag)}' "); $this->dbService->query("DELETE FROM {$this->dbService->prefixTable('acls')} WHERE page_tag='{$this->dbService->escape($tag)}' "); $this->dbService->query("DELETE FROM {$this->dbService->prefixTable('triples')} WHERE `resource`='{$this->dbService->escape($tag)}' and `property`='" . TripleStore::TYPE_URI . "' and `value`='" . EntryManager::TRIPLES_ENTRY_ID . "'"); + $this->dbService->query("DELETE FROM {$this->dbService->prefixTable('triples')} WHERE `resource`='{$this->dbService->escape($tag)}' and `property`='" . TripleStore::TYPE_URI . "' and `value`='" . EntryManager::TRIPLES_ENTRY_ID . "'"); $this->dbService->query("DELETE FROM {$this->dbService->prefixTable('triples')} WHERE `resource`='{$this->dbService->escape($tag)}' and `property`='http://outils-reseaux.org/_vocabulary/metadata'"); $this->dbService->query("DELETE FROM {$this->dbService->prefixTable('referrers')} WHERE page_tag='{$this->dbService->escape($tag)}' "); $this->tagsManager->deleteAll($tag); @@ -407,8 +407,8 @@ public function getOwner($tag = '', $time = '') $timeQuery = $time ? "time = '{$this->dbService->escape($time)}'" : "latest = 'Y'"; $page = $this->dbService->loadSingle( "SELECT `owner` FROM {$this->dbService->prefixTable('pages')} " . - "WHERE tag = '{$this->dbService->escape($tag)}' AND {$timeQuery} " . - 'LIMIT 1' + "WHERE tag = '{$this->dbService->escape($tag)}' AND {$timeQuery} " . + "LIMIT 1" ); $this->ownersCache[$tag] = $page['owner'] ?? null; } @@ -499,18 +499,18 @@ private function checkEntriesACL(array $pages, ?string $tag = null, ?string $use } $pages = array_map(function ($page) use ($guard, $allEntriesTags, $userNameForCheckingACL) { return (isset($page['tag']) && - in_array($page['tag'], $allEntriesTags) + in_array($page['tag'], $allEntriesTags) ) ? $guard->checkAcls($page, $page['tag'], $userNameForCheckingACL) - : $page; + : $page; }, $pages); return $pages; } - private function duplicate($sourceTag, $destinationTag) : boolean + private function duplicate($sourceTag, $destinationTag): bool { $result = false; - $this->wiki->LogAdministrativeAction($this->authController->getLoggedUserName(), "Duplication de la page \"\"" . $sourceTag . "\"\" vers la page \"\"".$destinationTag."\"\""); + $this->wiki->LogAdministrativeAction($this->authController->getLoggedUserName(), "Duplication de la page \"\"" . $sourceTag . "\"\" vers la page \"\"" . $destinationTag . "\"\""); return $result; } } diff --git a/lang/yeswiki_fr.php b/lang/yeswiki_fr.php index 906f3fc98..286644e16 100644 --- a/lang/yeswiki_fr.php +++ b/lang/yeswiki_fr.php @@ -629,4 +629,11 @@ 'REACTION_TITLE_PARAM_NEEDED' => 'Le paramètre \'titre\' est obligatoire', 'REACTION_BAD_IMAGE_FORMAT' => 'Mauvais format d\'image : doit être un fichier, un icône utf8 ou une classe Fontawesome', 'REACTION_NO_IMAGE' => 'Image manquante', -]; \ No newline at end of file + + // Duplication + 'FILES_TO_DUPLICATE' => 'Fichiers à dupliquer', + 'TOTAL_SIZE' => 'taille totale', + 'DUPLICATE_AND_OPEN' => 'Dupliquer et afficher', + 'DUPLICATE_AND_EDIT' => 'Dupliquer et éditer', + 'PAGE_TAG_TO_DUPLICATE' => 'Identifiant de la page dupliquée', +]; diff --git a/templates/handlers/duplicate-inner.twig b/templates/handlers/duplicate-inner.twig new file mode 100644 index 000000000..0053d4bee --- /dev/null +++ b/templates/handlers/duplicate-inner.twig @@ -0,0 +1,16 @@ +
+
+ +
+ +
+
+ {{ _t('FILES_TO_DUPLICATE') }} ({{ _t('TOTAL_SIZE') ~ ' ' ~ totalSize }}) +
    + {% for a in attachments %} +
  1. {{a.path}} ({{a.humanSize}})
  2. + {% endfor %} +
+ + +
diff --git a/templates/handlers/duplicate.twig b/templates/handlers/duplicate.twig new file mode 100644 index 000000000..8c39926b5 --- /dev/null +++ b/templates/handlers/duplicate.twig @@ -0,0 +1,4 @@ +
+{% if title %}

{{ title }}

{% endif %} +{{ output | raw }} +
diff --git a/tools/bazar/services/EntryManager.php b/tools/bazar/services/EntryManager.php index 2612688c7..78c0e1ad1 100644 --- a/tools/bazar/services/EntryManager.php +++ b/tools/bazar/services/EntryManager.php @@ -1181,11 +1181,10 @@ function ($attributeName) { return $entriesIds; } - private function duplicate($sourceTag, $destinationTag) : boolean + private function duplicate($sourceTag, $destinationTag): bool { $result = false; - $this->wiki->LogAdministrativeAction($this->authController->getLoggedUserName(), "Duplication de la fiche \"\"" . $sourceTag . "\"\" vers la fiche \"\"".$destinationTag."\"\""); + $this->wiki->LogAdministrativeAction($this->authController->getLoggedUserName(), "Duplication de la fiche \"\"" . $sourceTag . "\"\" vers la fiche \"\"" . $destinationTag . "\"\""); return $result; } - } diff --git a/tools/templates/actions/barreredaction.php b/tools/templates/actions/barreredaction.php index fd7a68fb7..adc10c98f 100755 --- a/tools/templates/actions/barreredaction.php +++ b/tools/templates/actions/barreredaction.php @@ -83,7 +83,7 @@ } } } - $options['linkduplicate'] = $this->href('', 'api/' . $page . '/duplicate'); + $options['linkduplicate'] = $this->href('duplicate', $page); $options['linkshare'] = $this->href("share", $page); $options['userIsOwner'] = $this->UserIsOwner($page); $options['userIsAdmin'] = $this->UserIsAdmin(); diff --git a/tools/templates/lang/templates_fr.inc.php b/tools/templates/lang/templates_fr.inc.php index eba03eab4..83f924c79 100644 --- a/tools/templates/lang/templates_fr.inc.php +++ b/tools/templates/lang/templates_fr.inc.php @@ -51,8 +51,8 @@ 'TEMPLATE_FOR_MEMBERS_OF_GROUP' => 'Pour les membres du groupe', 'TEMPLATE_SEE_ATTACHED_FILES' => 'Voir les fichiers attachés à la page', 'TEMPLATE_DUPLICATE' => 'Dupliquer', - 'TEMPLATE_DUPLICATE_AND_SEE' => 'Dupliquer localement et voir la page dupliquée', - 'TEMPLATE_DUPLICATE_AND_EDIT' => 'Dupliquer localement et éditer la page dupliquée', + 'TEMPLATE_DUPLICATE_PAGE' => 'Dupliquer la page', + 'TEMPLATE_DUPLICATE_LOCALLY' => 'Dupliquer dans ce YesWiki', 'TEMPLATE_DUPLICATE_IN_ANOTHER_WIKI' => 'Dupliquer dans un autre YesWiki', // action/diaporama diff --git a/tools/templates/templates/barreredaction_basic.twig b/tools/templates/templates/barreredaction_basic.twig index 4324b5616..0e03c7596 100644 --- a/tools/templates/templates/barreredaction_basic.twig +++ b/tools/templates/templates/barreredaction_basic.twig @@ -19,7 +19,7 @@ {% endif %} - {{ _t('TEMPLATE_DUPLICATE_AND_SEE') }} - - -
  • - - {{ _t('TEMPLATE_DUPLICATE_AND_EDIT') }} + {{ _t('TEMPLATE_DUPLICATE_LOCALLY') }}
  • {{ _t('TEMPLATE_DUPLICATE_IN_ANOTHER_WIKI') }} From a3f1b751cf4f1f5b2f60bdde54334ef9b3c16ac5 Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 03/27] feat(duplication): local duplication is almost finished --- handlers/DuplicateHandler.php | 96 +++++++-- includes/services/ImportFilesManager.php | 186 ++++++++++++------ lang/yeswiki_fr.php | 8 +- templates/handlers/duplicate-inner.twig | 21 +- .../javascripts/inputs/image-field.js | 9 +- tools/bazar/services/ListManager.php | 15 ++ tools/bazar/templates/entries/view.twig | 30 +++ tools/templates/lang/templates_fr.inc.php | 6 +- .../templates/barreredaction_basic.twig | 2 - 9 files changed, 281 insertions(+), 92 deletions(-) diff --git a/handlers/DuplicateHandler.php b/handlers/DuplicateHandler.php index 581166f4b..3cb016345 100644 --- a/handlers/DuplicateHandler.php +++ b/handlers/DuplicateHandler.php @@ -1,9 +1,9 @@ wiki->page) { $output = $this->render('@templates\alert-message.twig', [ - 'type' => 'info', + 'type' => 'warning', 'message' => str_replace( ["{beginLink}", "{endLink}"], - ["wiki->href('edit')}\">", ""], + ["wiki->href('')}\">", ""], _t("NOT_FOUND_PAGE") ), ]); - } elseif ($this->getService(AclService::class)->hasAccess('read', $this->wiki->GetPageTag())) { - $title = _t('TEMPLATE_DUPLICATE_PAGE') . ' ' . $this->wiki->GetPageTag(); - $attachments = $this->importManager->findDirectLinkAttachements($this->wiki->page['tag']); - $totalSize = 0; - foreach ($attachments as $a) { - $totalSize = $totalSize + $a['size']; - } - $output .= $this->render('@core/handlers/duplicate-inner.twig', [ - 'attachments' => $attachments, - 'totalSize' => $this->importManager->humanFilesize($totalSize), - 'isEntry' => $this->getService(EntryManager::class)->isEntry($this->wiki->GetPageTag()), - 'toExternalWiki' => isset($_GET['toUrl']) && $_GET['toUrl'] == "1" - ]); - } else { // if no read access to the page + } elseif (!$this->getService(AclService::class)->hasAccess('read', $this->wiki->GetPageTag())) { + // if no read access to the page if ($contenu = $this->getService(PageManager::class)->getOne("PageLogin")) { // si une page PageLogin existe, on l'affiche $output .= $this->wiki->Format($contenu["body"]); @@ -56,7 +44,81 @@ public function run() . $this->wiki->Format('{{login signupurl="0"}}' . "\n\n") . '' . "\n"; } + } elseif (!empty($_POST)) { + try { + $data = $this->importManager->checkPostData($_POST); + if (!$this->getService(AclService::class)->hasAccess('write', $_POST['pageTag'])) { + throw new \Exception(_t('LOGIN_NOT_AUTORIZED_EDIT') . ' ' . $data['pageTag']); + } + switch ($data['type']) { + + case 'list': + $list = $this->getService(ListManager::class)->getOne($this->wiki->getPageTag()); + $this->getService(ListManager::class)->create($data['pageTitle'], $list['label'], $data['pageTag']); + break; + + case 'entry': + $entry = $this->getService(EntryManager::class)->getOne($this->wiki->getPageTag()); + $entry['id_fiche'] = $data['pageTag']; + $entry['bf_titre'] = $data['pageTitle']; + $entry['antispam'] = 1; + $this->getService(EntryManager::class)->create($entry['id_typeannonce'], $entry); + $this->importManager->duplicateFiles($this->wiki->getPageTag(), $data['pageTag']); + break; + + default: + case 'page': + $this->getService(PageManager::class)->save($data['pageTag'], $this->wiki->page['body']); + $this->importManager->duplicateFiles($this->wiki->getPageTag(), $data['pageTag']); + break; + } + // TODO: duplicate acls and metadatas + if ($data['duplicate-action'] == 'edit') { + $this->wiki->Redirect($this->wiki->href('edit', $data['pageTag'])); + return; + } + $this->wiki->Redirect($this->wiki->href('', $data['pageTag'])); + return; + } catch (\Throwable $th) { + $output = $this->render('@templates\alert-message-with-back.twig', [ + 'type' => 'warning', + 'message' => $th->getMessage(), + ]); + } + } elseif ($this->getService(AclService::class)->hasAccess('read', $this->wiki->GetPageTag())) { + $isEntry = $this->getService(EntryManager::class)->isEntry($this->wiki->GetPageTag()); + $isList = $this->getService(ListManager::class)->isList($this->wiki->GetPageTag()); + $type = $isEntry ? 'entry' : ($isList ? 'list' : 'page'); + $pageTitle = ''; + if ($isEntry) { + $title = _t('TEMPLATE_DUPLICATE_ENTRY') . ' ' . $this->wiki->GetPageTag(); + $entry = $this->getService(EntryManager::class)->getOne($this->wiki->GetPageTag()); + $pageTitle = $entry['bf_titre'] . ' (' . _t('DUPLICATE') . ')'; + $proposedTag = genere_nom_wiki($pageTitle); + } elseif ($isList) { + $title = _t('TEMPLATE_DUPLICATE_LIST') . ' ' . $this->wiki->GetPageTag(); + $list = $this->getService(ListManager::class)->getOne($this->wiki->GetPageTag()); + $pageTitle = $list['titre_liste'] . ' (' . _t('DUPLICATE') . ')'; + $proposedTag = genere_nom_wiki('Liste ' . $pageTitle); + } else { // page + $title = _t('TEMPLATE_DUPLICATE_PAGE') . ' ' . $this->wiki->GetPageTag(); + $proposedTag = genere_nom_wiki($this->wiki->GetPageTag() . ' ' . _t('DUPLICATE')); + } + $attachments = $this->importManager->findFiles($this->wiki->page['tag']); + $totalSize = 0; + foreach ($attachments as $a) { + $totalSize = $totalSize + $a['size']; + } + $output .= $this->render('@core/handlers/duplicate-inner.twig', [ + 'proposedTag' => $proposedTag, + 'attachments' => $attachments, + 'pageTitle' => $pageTitle, + 'totalSize' => $this->importManager->humanFilesize($totalSize), + 'type' => $type, + 'toExternalWiki' => isset($_GET['toUrl']) && $_GET['toUrl'] == "1" + ]); } + // in ajax request for modal, no title if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { $title = ''; diff --git a/includes/services/ImportFilesManager.php b/includes/services/ImportFilesManager.php index 7095ee2b5..c5ea2becf 100644 --- a/includes/services/ImportFilesManager.php +++ b/includes/services/ImportFilesManager.php @@ -2,9 +2,12 @@ namespace YesWiki\Core\Service; +use YesWiki\Bazar\Field\FileField; +use YesWiki\Bazar\Field\ImageField; use YesWiki\Bazar\Field\TextareaField; use YesWiki\Bazar\Service\FormManager; use YesWiki\Bazar\Service\EntryManager; +use YesWiki\Bazar\Service\ListManager; use YesWiki\Core\Service\PageManager; use YesWiki\Wiki; @@ -21,7 +24,7 @@ class ImportFilesManager public function __construct(Wiki $wiki) { $this->wiki = $wiki; - $this->uploadPath = null; + $this->uploadPath = $this->getLocalFileUploadPath(); } /** @@ -31,10 +34,6 @@ public function __construct(Wiki $wiki) */ private function getLocalFileUploadPath() { - if ($this->uploadPath !== null) { - return $this->uploadPath; - } - $attachConfig = $this->wiki->config['attach_config']; if (!is_array($attachConfig)) { @@ -89,49 +88,60 @@ private function cURLDownload($from, $to, $overwrite = false) } /** - * Return fields that may contain attachments to import (body for wikipage, or textelong fields for bazar entries) + * Return fields that may contain attachments to import (fichier, image, or textelong fields for bazar entries) * - * @param array $wikiPage page or entry content as an array - * @return array keys of $wikiPage that may contain attachments to import + * @param array $id + * @return array keys of fields that may contain attachments to import */ - public function getTextFieldsFromWikiPage($wikiPage) + public function getUploadFieldsFromEntry($id) { $fields = []; - if (!empty($wikiPage['tag'])) { // classic wiki page - $fields[] = 'body'; - } elseif (!empty($wikiPage['id_fiche'])) { // bazar entry + $entry = $this->wiki->services->get(EntryManager::class)->getOne($id); + if (!empty($entry['id_fiche'])) { // bazar entry $formManager = $this->wiki->services->get(FormManager::class); - $form = $formManager->getOne($wikiPage['id_typeannonce']); + $form = $formManager->getOne($entry['id_typeannonce']); // find fields that are textareas foreach ($form['prepared'] as $field) { - if ($field instanceof TextareaField) { - $fields[] = $field->getName(); + if ($field instanceof TextareaField or $field instanceof ImageField or $field instanceof FileField) { + $fields[] = [ + 'id' => $field->getPropertyName(), + 'type' => $field->getType() + ]; } } } return $fields; } + public function findFilesInUploadField($fieldValue) + { + $f = $this->uploadPath . '/' . $fieldValue; + if (file_exists($f)) { + $size = filesize($f); + $humanSize = $this->humanFilesize($size); + return ['path' => $f, 'size' => $size, 'humanSize' => $humanSize]; + } else { + return []; + } + } + /** - * Get attachements from raw page content + * find files in wiki text * - * @param string $tag page id - * @return array attachments filenames + * @param string $wikiTag + * @param string $wikiText + * @return array files */ - public function findDirectLinkAttachements($tag = '') + public function findFilesInWikiText($tag, $wikiText) { - if (empty(trim($tag))) { - $tag = $this->wiki->GetPageTag(); - } - $rawContent = $this->wiki->services->get(PageManager::class)->getOne($tag)['body']; + $filesMatched = []; $regex = '#\{\{attach.*file="(.*)".*\}\}#Ui'; preg_match_all( $regex, - $rawContent, + $wikiText, $attachments ); if (is_array($attachments[1])) { - $filesMatched = []; foreach ($attachments[1] as $a) { $ext = pathinfo($a, PATHINFO_EXTENSION); $filename = pathinfo($a, PATHINFO_FILENAME); @@ -151,9 +161,97 @@ public function findDirectLinkAttachements($tag = '') } } } + $fileUrlRegex = '#' . preg_quote(str_replace('?', '', $this->wiki->config['base_url']), '#') . + '(files/.*\.[a-zA-Z0-9]{1,16}\b([-a-zA-Z0-9!@:%_\+.~\#?&\/\/=]*))#Ui'; + preg_match_all( + $fileUrlRegex, + $wikiText, + $fileUrls + ); + foreach ($fileUrls[1] as $f) { + if (file_exists($f)) { + $size = filesize($f); + $humanSize = $this->humanFilesize($size); + $filesMatched[] = ['path' => $f, 'size' => $size, 'humanSize' => $humanSize]; + } + } return $filesMatched; } + /** + * Get file attachements from pageTag + * + * @param string $tag page id + * @return array attachments filenames + */ + public function findFiles($tag = '') + { + $files = []; + if (empty(trim($tag))) { + $tag = $this->wiki->GetPageTag(); + } + if ($this->wiki->services->get(EntryManager::class)->isEntry($tag)) { + // bazar + $fields = $this->getUploadFieldsFromEntry($tag); + $entry = $this->wiki->services->get(EntryManager::class)->getOne($tag); + foreach ($fields as $f) { + if ($f['type'] == 'image' || $f['type'] == 'fichier') { + if (!empty($fi = $this->findFilesInUploadField($entry[$f['id']]))) { + $files[] = $fi; + } + } elseif ($f['type'] == 'textelong') { + if (!empty($fi = $this->findFilesInWikiText($tag, $entry[$f['id']]))) { + $files = array_merge($files, $fi); + } + } + } + } elseif (!$this->wiki->services->get(ListManager::class)->isList($tag)) { // page + $wikiText = $this->wiki->services->get(PageManager::class)->getOne($tag)['body']; + if ($fi = $this->findFilesInWikiText($tag, $wikiText)) { + $files[] = $fi; + } + } + return $files; + } + + public function duplicateFiles($fromTag, $toTag) + { + $files = $this->findFiles($fromTag); + foreach ($files as $f) { + $newPath = preg_replace( + '~files/' . preg_quote($fromTag, '~') . '_~Ui', + 'files/' . $toTag . '_', + $f['path'] + ); + // if the file name has not changed, we add newPageTag_ as filename prefix + if ($f['path'] == $newPath) { + $newPath = str_replace('files/', 'files/' . $toTag . '_', $newPath); + } + copy($f['path'], $newPath); + } + } + + public function checkPostData($data) + { + if (empty($data['type']) || !in_array($data['type'], ['page', 'list', 'entry'])) { + throw new \Exception(_t('NO_VALID_DATA_TYPE')); + } + if (empty($data['pageTag'])) { + throw new \Exception(_t('EMPTY_PAGE_TAG')); + } + if ($data['type'] != 'page' && empty($data['pageTitle'])) { + throw new \Exception(_t('EMPTY_PAGE_TITLE')); + } + $page = $this->wiki->services->get(PageManager::class)->getOne($data['pageTag']); + if ($page) { + throw new \Exception($data['pageTag'] . ' ' . _t('ALREADY_EXISTING')); + } + if (empty($data['duplicate-action']) || !in_array($data['duplicate-action'], ['open', 'edit'])) { + throw new \Exception(_t('NO_DUPLICATE_ACTION') . '.'); + } + return $data; + } + public function humanFilesize($bytes, $decimals = 2) { $factor = floor((strlen($bytes) - 1) / 3); @@ -179,44 +277,6 @@ public function downloadDirectLinkAttachment($remoteUrl, $filename, $overwrite = return $this->cURLDownload($remoteFileUrl, $saveFileLoc, $overwrite); } - /** - * Find file attachments in page or bazar entry - * It finds attachments linked with /download links - * - * @param string $remoteUrl distant url - * @param array $wikiPage page or entry content as an array - * @param boolean $transform transform attachments urls for their new location (default:false) - * @return array all file attachments - */ - public function findHiddenAttachments($remoteUrl, &$wikiPage, $transform = false) - { - preg_match_all( - '#(?:href|src)="' . preg_quote($remoteUrl, '#') . '\?.+/download&(?:amp;)?file=(?P.*)"#Ui', - $wikiPage['html_output'], - $htmlMatches - ); - $attachments = $htmlMatches['filename']; - - $wikiRegex = '#="' . preg_quote($remoteUrl, '#') - . '(?P\?.+/download&(?:amp;)?file=(?P.*))"#Ui'; - - $contentKeys = $this->getTextFieldsFromWikiPage($wikiPage); - foreach ($contentKeys as $key) { - preg_match_all($wikiRegex, $wikiPage[$key], $wikiMatches); - $attachments = array_merge($attachments, $wikiMatches['filename']); - } - - $attachments = array_unique($attachments); - - if ($transform) { - foreach ($contentKeys as $key) { - $wikiPage[$key] = preg_replace($wikiRegex, '="' . $this->wiki->getBaseUrl() . '${trail}"', $wikiPage[$key]); - } - } - - return $attachments; - } - /** * Generate local path and download hidden attachments * It downloads attachments linked with /download links diff --git a/lang/yeswiki_fr.php b/lang/yeswiki_fr.php index 286644e16..07978ab89 100644 --- a/lang/yeswiki_fr.php +++ b/lang/yeswiki_fr.php @@ -631,9 +631,15 @@ 'REACTION_NO_IMAGE' => 'Image manquante', // Duplication + 'DUPLICATE' => 'copie', 'FILES_TO_DUPLICATE' => 'Fichiers à dupliquer', 'TOTAL_SIZE' => 'taille totale', 'DUPLICATE_AND_OPEN' => 'Dupliquer et afficher', 'DUPLICATE_AND_EDIT' => 'Dupliquer et éditer', - 'PAGE_TAG_TO_DUPLICATE' => 'Identifiant de la page dupliquée', + 'NO_DUPLICATE_ACTION' => 'Pas d\'action après duplication indiquée (duplicate-action)', + 'PAGE_TITLE_TO_DUPLICATE' => 'Titre après duplication', + 'PAGE_TAG_TO_DUPLICATE' => 'Identifiant après duplication', + 'NO_VALID_DATA_TYPE' => 'Pas de type de données valide', + 'EMPTY_PAGE_TAG' => 'L\'identifiant ne peut pas être vide', + 'EMPTY_PAGE_TITLE' => 'Le titre ne peut pas être vide', ]; diff --git a/templates/handlers/duplicate-inner.twig b/templates/handlers/duplicate-inner.twig index 0053d4bee..6e5ae2ca7 100644 --- a/templates/handlers/duplicate-inner.twig +++ b/templates/handlers/duplicate-inner.twig @@ -1,16 +1,27 @@ -
    + + {% if pageTitle %} +
    + +
    + +
    +
    + {% endif %}
    - +
    + {% if attachments|length > 0 %} {{ _t('FILES_TO_DUPLICATE') }} ({{ _t('TOTAL_SIZE') ~ ' ' ~ totalSize }})
      {% for a in attachments %} -
    1. {{a.path}} ({{a.humanSize}})
    2. +
    3. {{a.path|replace({'files/': ''})}} ({{a.humanSize}})
    4. {% endfor %}
    - - + {% endif %} + + +
    diff --git a/tools/bazar/presentation/javascripts/inputs/image-field.js b/tools/bazar/presentation/javascripts/inputs/image-field.js index a267686da..b39899ae9 100644 --- a/tools/bazar/presentation/javascripts/inputs/image-field.js +++ b/tools/bazar/presentation/javascripts/inputs/image-field.js @@ -60,7 +60,12 @@ function handleFileSelect(evt) { css = '' // Render thumbnail. const span = document.createElement('span') - span.innerHTML = `${escape(theFile.name)}` + span.innerHTML = `` document.getElementById(`img-${id}`).innerHTML = span.innerHTML }) } @@ -72,6 +77,6 @@ function handleFileSelect(evt) { } const imageinputs = document.getElementsByClassName('yw-image-upload') -for (let i = 0; i < imageinputs.length; i++) { +for (let i = 0; i < imageinputs.length; i += 1) { imageinputs.item(i).addEventListener('change', handleFileSelect, false) } diff --git a/tools/bazar/services/ListManager.php b/tools/bazar/services/ListManager.php index 091a29597..ffed3a167 100644 --- a/tools/bazar/services/ListManager.php +++ b/tools/bazar/services/ListManager.php @@ -44,6 +44,11 @@ public function __construct( $this->cachedLists = []; } + public function isList($id): bool + { + return boolval($this->tripleStore->exist($id, TripleStore::TYPE_URI, self::TRIPLES_LIST_ID, '', '')); + } + public function getOne($id): ?array { if (isset($this->cachedLists[$id])) { @@ -99,6 +104,16 @@ public function create($title, $nodes) if ($this->securityController->isWikiHibernated()) { throw new \Exception(_t('WIKI_IN_HIBERNATION')); } + $id = $id ?? genere_nom_wiki('Liste ' . $title); + + $values = $this->sanitizeHMTL($values); + + if (YW_CHARSET !== 'UTF-8') { + $values = array_map(function ($value) { + return mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'); + }, $values); + $title = mb_convert_encoding($title, 'UTF-8', 'ISO-8859-1'); + } $id = genere_nom_wiki('List' . $title); $this->pageManager->save($id, json_encode([ diff --git a/tools/bazar/templates/entries/view.twig b/tools/bazar/templates/entries/view.twig index 7d09e2d01..928a8ab8d 100644 --- a/tools/bazar/templates/entries/view.twig +++ b/tools/bazar/templates/entries/view.twig @@ -63,6 +63,36 @@ {{ _t('BAZ_MODIFIER') }} {% endif %} + + + {% if canDelete %} 'Voir les fichiers attachés à la page', 'TEMPLATE_DUPLICATE' => 'Dupliquer', 'TEMPLATE_DUPLICATE_PAGE' => 'Dupliquer la page', - 'TEMPLATE_DUPLICATE_LOCALLY' => 'Dupliquer dans ce YesWiki', - 'TEMPLATE_DUPLICATE_IN_ANOTHER_WIKI' => 'Dupliquer dans un autre YesWiki', + 'TEMPLATE_DUPLICATE_LIST' => 'Dupliquer la liste', + 'TEMPLATE_DUPLICATE_ENTRY' => 'Dupliquer la fiche', + 'TEMPLATE_DUPLICATE_LOCALLY' => 'Dans ce YesWiki', + 'TEMPLATE_DUPLICATE_IN_ANOTHER_WIKI' => 'Dans un autre YesWiki', // action/diaporama 'DIAPORAMA_PAGE_PARAM_MISSING' => 'Action diaporama : paramêtre "page" obligatoire.', diff --git a/tools/templates/templates/barreredaction_basic.twig b/tools/templates/templates/barreredaction_basic.twig index 0e03c7596..e9dd434b8 100644 --- a/tools/templates/templates/barreredaction_basic.twig +++ b/tools/templates/templates/barreredaction_basic.twig @@ -33,7 +33,6 @@
  • {{ _t('TEMPLATE_DUPLICATE_LOCALLY') }} @@ -42,7 +41,6 @@
  • {{ _t('TEMPLATE_DUPLICATE_IN_ANOTHER_WIKI') }} From 6bed40776ddb421dbc043b3b06bb9e064b75e86e Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 04/27] feat(duplicate): works for entries and rename manager --- handlers/DuplicateHandler.php | 39 +--- ...ilesManager.php => DuplicationManager.php} | 168 ++++++------------ 2 files changed, 58 insertions(+), 149 deletions(-) rename includes/services/{ImportFilesManager.php => DuplicationManager.php} (63%) diff --git a/handlers/DuplicateHandler.php b/handlers/DuplicateHandler.php index 3cb016345..2af43550e 100644 --- a/handlers/DuplicateHandler.php +++ b/handlers/DuplicateHandler.php @@ -5,7 +5,7 @@ use YesWiki\Bazar\Service\EntryManager; use YesWiki\Bazar\Service\ListManager; use YesWiki\Core\Service\PageManager; -use YesWiki\Core\Service\ImportFilesManager; +use YesWiki\Core\Service\DuplicationManager; use YesWiki\Core\Controller\AuthController; use YesWiki\Core\YesWikiHandler; @@ -13,13 +13,13 @@ class DuplicateHandler extends YesWikiHandler { protected $authController; protected $entryController; - protected $importManager; + protected $duplicationManager; public function run() { $this->authController = $this->getService(AuthController::class); $this->entryController = $this->getService(EntryController::class); - $this->importManager = $this->getService(ImportFilesManager::class); + $this->duplicationManager = $this->getService(DuplicationManager::class); $output = $title = ''; if (!$this->wiki->page) { $output = $this->render('@templates\alert-message.twig', [ @@ -46,33 +46,8 @@ public function run() } } elseif (!empty($_POST)) { try { - $data = $this->importManager->checkPostData($_POST); - if (!$this->getService(AclService::class)->hasAccess('write', $_POST['pageTag'])) { - throw new \Exception(_t('LOGIN_NOT_AUTORIZED_EDIT') . ' ' . $data['pageTag']); - } - switch ($data['type']) { - - case 'list': - $list = $this->getService(ListManager::class)->getOne($this->wiki->getPageTag()); - $this->getService(ListManager::class)->create($data['pageTitle'], $list['label'], $data['pageTag']); - break; - - case 'entry': - $entry = $this->getService(EntryManager::class)->getOne($this->wiki->getPageTag()); - $entry['id_fiche'] = $data['pageTag']; - $entry['bf_titre'] = $data['pageTitle']; - $entry['antispam'] = 1; - $this->getService(EntryManager::class)->create($entry['id_typeannonce'], $entry); - $this->importManager->duplicateFiles($this->wiki->getPageTag(), $data['pageTag']); - break; - - default: - case 'page': - $this->getService(PageManager::class)->save($data['pageTag'], $this->wiki->page['body']); - $this->importManager->duplicateFiles($this->wiki->getPageTag(), $data['pageTag']); - break; - } - // TODO: duplicate acls and metadatas + $data = $this->duplicationManager->checkPostData($_POST); + $this->duplicationManager->duplicateLocally($data); if ($data['duplicate-action'] == 'edit') { $this->wiki->Redirect($this->wiki->href('edit', $data['pageTag'])); return; @@ -104,7 +79,7 @@ public function run() $title = _t('TEMPLATE_DUPLICATE_PAGE') . ' ' . $this->wiki->GetPageTag(); $proposedTag = genere_nom_wiki($this->wiki->GetPageTag() . ' ' . _t('DUPLICATE')); } - $attachments = $this->importManager->findFiles($this->wiki->page['tag']); + $attachments = $this->duplicationManager->findFiles($this->wiki->page['tag']); $totalSize = 0; foreach ($attachments as $a) { $totalSize = $totalSize + $a['size']; @@ -113,7 +88,7 @@ public function run() 'proposedTag' => $proposedTag, 'attachments' => $attachments, 'pageTitle' => $pageTitle, - 'totalSize' => $this->importManager->humanFilesize($totalSize), + 'totalSize' => $this->duplicationManager->humanFilesize($totalSize), 'type' => $type, 'toExternalWiki' => isset($_GET['toUrl']) && $_GET['toUrl'] == "1" ]); diff --git a/includes/services/ImportFilesManager.php b/includes/services/DuplicationManager.php similarity index 63% rename from includes/services/ImportFilesManager.php rename to includes/services/DuplicationManager.php index c5ea2becf..dd8139c4a 100644 --- a/includes/services/ImportFilesManager.php +++ b/includes/services/DuplicationManager.php @@ -11,14 +11,14 @@ use YesWiki\Core\Service\PageManager; use YesWiki\Wiki; -class ImportFilesManager +class DuplicationManager { protected $uploadPath; protected $wiki; /** - * ImportManager constructor + * DuplicationManager constructor * @param Wiki $wiki the injected Wiki instance */ public function __construct(Wiki $wiki) @@ -49,44 +49,6 @@ private function getLocalFileUploadPath() return $this->uploadPath; } - /** - * Download file url to local wiki using cURL - * - * @param string $from file url - * @param string $to local path - * @param boolean $overwrite overwrite existing file ? (default:false) - * @return void - */ - private function cURLDownload($from, $to, $overwrite = false) - { - $output = ''; - if (file_exists($to)) { - if ($overwrite) { - $output .= _t('FILE') . ' ' . $to . ' ' . _t('FILE_OVERWRITE') . '.'; - } else { - $output .= _t('FILE') . ' ' . $to . ' ' . _t('FILE_NO_OVERWRITE') . '.'; - return $output; - } - } - - // Do cURL transfer - $fp = fopen($to, 'wb'); - $ch = curl_init($from); - curl_setopt($ch, CURLOPT_FILE, $fp); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_FAILONERROR, true); - curl_exec($ch); - $err = curl_error($ch); - curl_close($ch); - fclose($fp); - - if ($err) { - unlink($to); - throw new \Exception($output . _t('ERROR_DOWNLOADING') . ' ' . $from . ': ' . $err . "\n" . _t('REMOVING_CORRUPTED_FILE') . ' ' . $to); - } - return $output; - } - /** * Return fields that may contain attachments to import (fichier, image, or textelong fields for bazar entries) * @@ -162,7 +124,7 @@ public function findFilesInWikiText($tag, $wikiText) } } $fileUrlRegex = '#' . preg_quote(str_replace('?', '', $this->wiki->config['base_url']), '#') . - '(files/.*\.[a-zA-Z0-9]{1,16}\b([-a-zA-Z0-9!@:%_\+.~\#?&\/\/=]*))#Ui'; + '(' . $this->uploadPath . '/.*\.[a-zA-Z0-9]{1,16}\b([-a-zA-Z0-9!@:%_\+.~\#?&\/\/=]*))#Ui'; preg_match_all( $fileUrlRegex, $wikiText, @@ -208,7 +170,7 @@ public function findFiles($tag = '') } elseif (!$this->wiki->services->get(ListManager::class)->isList($tag)) { // page $wikiText = $this->wiki->services->get(PageManager::class)->getOne($tag)['body']; if ($fi = $this->findFilesInWikiText($tag, $wikiText)) { - $files[] = $fi; + $files = array_merge($files, $fi); } } return $files; @@ -217,18 +179,24 @@ public function findFiles($tag = '') public function duplicateFiles($fromTag, $toTag) { $files = $this->findFiles($fromTag); + $doneFiles = []; foreach ($files as $f) { $newPath = preg_replace( - '~files/' . preg_quote($fromTag, '~') . '_~Ui', - 'files/' . $toTag . '_', + '~' . $this->uploadPath . '/' . preg_quote($fromTag, '~') . '_~Ui', + $this->uploadPath . '/' . $toTag . '_', $f['path'] ); // if the file name has not changed, we add newPageTag_ as filename prefix if ($f['path'] == $newPath) { - $newPath = str_replace('files/', 'files/' . $toTag . '_', $newPath); + $newPath = str_replace($this->uploadPath . '/', $this->uploadPath . '/' . $toTag . '_', $newPath); } copy($f['path'], $newPath); + $doneFiles[] = [ + 'originalFile' => str_replace($this->uploadPath . '/', '', $f['path']), + 'duplicatedFile' => str_replace($this->uploadPath . '/', '', $newPath), + ]; } + return $doneFiles; } public function checkPostData($data) @@ -242,6 +210,9 @@ public function checkPostData($data) if ($data['type'] != 'page' && empty($data['pageTitle'])) { throw new \Exception(_t('EMPTY_PAGE_TITLE')); } + if (!$this->wiki->services->get(AclService::class)->hasAccess('write', $data['pageTag'])) { + throw new \Exception(_t('LOGIN_NOT_AUTORIZED_EDIT') . ' ' . $data['pageTag']); + } $page = $this->wiki->services->get(PageManager::class)->getOne($data['pageTag']); if ($page) { throw new \Exception($data['pageTag'] . ' ' . _t('ALREADY_EXISTING')); @@ -252,86 +223,49 @@ public function checkPostData($data) return $data; } - public function humanFilesize($bytes, $decimals = 2) + public function duplicateLocally($data) { - $factor = floor((strlen($bytes) - 1) / 3); - if ($factor > 0) { - $sz = 'KMGT'; - } - return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor - 1] . 'B'; - } + switch ($data['type']) { + case 'list': + $list = $this->wiki->services->get(ListManager::class)->getOne($this->wiki->getPageTag()); + $this->wiki->services->get(ListManager::class)->create($data['pageTitle'], $list['label'], $data['pageTag']); + break; - /** - * Generate distant file url and download to local file path - * - * @param string $remoteUrl distant file url - * @param string $filename file name - * @param boolean $overwrite overwrite existing file ? (default:false) - * @return void - */ - public function downloadDirectLinkAttachment($remoteUrl, $filename, $overwrite = false) - { - $remoteFileUrl = $remoteUrl . '/files/' . $filename; - $saveFileLoc = $this->getLocalFileUploadPath() . '/' . $filename; - - return $this->cURLDownload($remoteFileUrl, $saveFileLoc, $overwrite); - } - - /** - * Generate local path and download hidden attachments - * It downloads attachments linked with /download links - * - * @param string $remoteUrl distant url - * @param string $pageTag page tag - * @param string $lastPageUpdate last update time - * @param string $filename file name - * @param boolean $overwrite overwrite existing file ? (default:false) - * @return array all file attachments - */ - public function downloadHiddenAttachment($remoteUrl, $pageTag, $lastPageUpdate, $filename, $overwrite = false) - { - if (!class_exists('attach')) { - require_once("tools/attach/libs/attach.lib.php"); - } + case 'entry': + $files = $this->duplicateFiles($this->wiki->getPageTag(), $data['pageTag']); + $entry = $this->wiki->services->get(EntryManager::class)->getOne($this->wiki->getPageTag()); + dump($entry, $files); + $fields = $this->getUploadFieldsFromEntry($this->wiki->GetPageTag()); + foreach ($fields as $f) { + } - $this->wiki->tag = $pageTag; - $this->wiki->page = array('tag' => $pageTag, 'time' => $lastPageUpdate); + exit; - $remoteFileUrl = $remoteUrl . '?' . $pageTag . '/download&file=' . $filename; - $att = new \attach($this->wiki); - $att->file = $filename; - $newFilename = $att->GetFullFilename(true); + $entry['id_fiche'] = $data['pageTag']; + $entry['bf_titre'] = $data['pageTitle']; + $entry['antispam'] = 1; + $this->wiki->services->get(EntryManager::class)->create($entry['id_typeannonce'], $entry); + break; - $this->cURLDownload($remoteFileUrl, $newFilename, $overwrite); + default: + case 'page': + $newBody = $this->wiki->page['body']; + $files = $this->duplicateFiles($this->wiki->getPageTag(), $data['pageTag']); + foreach ($files as $f) { + $newBody = str_replace($f['originalFile'], $f['duplicatedFile'], $newBody); + } + $this->wiki->services->get(PageManager::class)->save($data['pageTag'], $newBody); + break; + } + // TODO: duplicate acls and metadatas } - - /** - * All type of attachment related to a page or a bazar entry - * - * @param string $remoteUrl distant url - * @param array $wikiPage page or entry content as an array - * @param boolean $overwrite overwrite existing file ? (default:false) - * @return void - */ - public function downloadAttachments($remoteUrl, &$wikiPage, $overwrite = false) + public function humanFilesize($bytes, $decimals = 2) { - // Handle Pictures and file attachments - $attachments = $this->findDirectLinkAttachements($remoteUrl, $wikiPage, true); - - if (count($attachments)) { - foreach ($attachments as $image) { - $this->downloadDirectLinkAttachment($remoteUrl, $image, $overwrite); - } - } - - // Downloading hidden attachments - $attachments = $this->findHiddenAttachments($remoteUrl, $wikiPage, true); - - if (!empty($attachments)) { - foreach ($attachments as $attachment) { - $this->downloadHiddenAttachment($remoteUrl, $wikiPage['id_fiche'], date("Y-m-d H:i:s"), $attachment, $overwrite); - } + $factor = floor((strlen($bytes) - 1) / 3); + if ($factor > 0) { + $sz = 'KMGT'; } + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor - 1] . 'B'; } } From 486d3cd6f73a6dcd4c821c69717a4d7cdedf45be Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 05/27] feat(duplication): duplicate acls and metadatas --- includes/services/DuplicationManager.php | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/includes/services/DuplicationManager.php b/includes/services/DuplicationManager.php index dd8139c4a..3c26a957d 100644 --- a/includes/services/DuplicationManager.php +++ b/includes/services/DuplicationManager.php @@ -257,7 +257,32 @@ public function duplicateLocally($data) $this->wiki->services->get(PageManager::class)->save($data['pageTag'], $newBody); break; } - // TODO: duplicate acls and metadatas + + // duplicate acls + foreach (['read', 'write', 'comment'] as $privilege) { + $values = $this->wiki->services->get(AclService::class)->load( + $this->wiki->getPageTag(), + $privilege + ); + + $this->wiki->services->get(AclService::class)->save( + $data['pageTag'], + $privilege, + $values['list'] + ); + } + + // duplicate metadatas and tags (TODO: is there more duplicable triples?) + $properties = [ + 'http://outils-reseaux.org/_vocabulary/metadata', + 'http://outils-reseaux.org/_vocabulary/tag' + ]; + foreach ($properties as $prop) { + $values = $this->wiki->services->get(TripleStore::class)->getAll($this->wiki->GetPageTag(), $prop, '', ''); + foreach ($values as $val) { + $this->wiki->services->get(TripleStore::class)->create($data['pageTag'], $prop, $val['value'], '', ''); + } + } } public function humanFilesize($bytes, $decimals = 2) From 32c1cd210685151b549d1ba8e117abea3796ac0f Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 06/27] feat(duplication): make entry duplicate and change field content --- includes/services/DuplicationManager.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/includes/services/DuplicationManager.php b/includes/services/DuplicationManager.php index 3c26a957d..77b6f69e0 100644 --- a/includes/services/DuplicationManager.php +++ b/includes/services/DuplicationManager.php @@ -234,13 +234,12 @@ public function duplicateLocally($data) case 'entry': $files = $this->duplicateFiles($this->wiki->getPageTag(), $data['pageTag']); $entry = $this->wiki->services->get(EntryManager::class)->getOne($this->wiki->getPageTag()); - dump($entry, $files); $fields = $this->getUploadFieldsFromEntry($this->wiki->GetPageTag()); foreach ($fields as $f) { + foreach ($files as $fi) { + $entry[$f['id']] = str_replace($fi['originalFile'], $fi['duplicatedFile'], $entry[$f['id']]); + } } - - exit; - $entry['id_fiche'] = $data['pageTag']; $entry['bf_titre'] = $data['pageTitle']; $entry['antispam'] = 1; From cd2807e799542d5c07cb874b2f78ec23c2b7c15c Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 07/27] feat(duplication): wip UI for external duplication --- handlers/DuplicateHandler.php | 3 +++ lang/yeswiki_fr.php | 4 ++++ templates/handlers/duplicate-inner.twig | 28 +++++++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/handlers/DuplicateHandler.php b/handlers/DuplicateHandler.php index 2af43550e..a1fcc6fbf 100644 --- a/handlers/DuplicateHandler.php +++ b/handlers/DuplicateHandler.php @@ -94,6 +94,9 @@ public function run() ]); } + if (isset($_GET['toUrl']) && $_GET['toUrl'] == "1") { + $title .= ' ' . _t('TO_ANOTHER_YESWIKI'); + } // in ajax request for modal, no title if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { $title = ''; diff --git a/lang/yeswiki_fr.php b/lang/yeswiki_fr.php index 07978ab89..cb6b51c2d 100644 --- a/lang/yeswiki_fr.php +++ b/lang/yeswiki_fr.php @@ -642,4 +642,8 @@ 'NO_VALID_DATA_TYPE' => 'Pas de type de données valide', 'EMPTY_PAGE_TAG' => 'L\'identifiant ne peut pas être vide', 'EMPTY_PAGE_TITLE' => 'Le titre ne peut pas être vide', + 'TO_ANOTHER_YESWIKI' => 'vers un autre YesWiki', + 'WIKI_URL' => 'Url du YesWiki de destination', + 'WIKI_URL_RECENT' => 'Entrez l\'url d\'un YesWiki assez récent (version > 4.5) pour recevoir la duplication', + 'VERIFY_WIKI' => 'Vérifier cette adresse', ]; diff --git a/templates/handlers/duplicate-inner.twig b/templates/handlers/duplicate-inner.twig index 6e5ae2ca7..24f27c197 100644 --- a/templates/handlers/duplicate-inner.twig +++ b/templates/handlers/duplicate-inner.twig @@ -1,4 +1,29 @@
    + {% if toExternalWiki %} +
    {{ _t('WIKI_URL_RECENT') }}.
    +
    + +
    +
    + + + + +
    +
    +
    + +
    {# hide the field while not connected #} + {% endif %} {% if pageTitle %}
    @@ -24,4 +49,7 @@ + {% if toExternalWiki %} +
    + {% endif %} From 59a0221e135c728b32b0812e216a1dd8ac747e5c Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 08/27] feat(duplication): only admins can duplicate --- handlers/DuplicateHandler.php | 5 +++++ includes/services/DuplicationManager.php | 4 ++-- lang/yeswiki_fr.php | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/handlers/DuplicateHandler.php b/handlers/DuplicateHandler.php index a1fcc6fbf..36add2cf8 100644 --- a/handlers/DuplicateHandler.php +++ b/handlers/DuplicateHandler.php @@ -60,6 +60,11 @@ public function run() 'message' => $th->getMessage(), ]); } + } elseif (!$this->wiki->UserIsAdmin()) { + $output .= $this->render('@templates\alert-message-with-back.twig', [ + 'type' => 'warning', + 'message' => _t('ONLY_ADMINS_CAN_DUPLICATE') . '.', + ]); } elseif ($this->getService(AclService::class)->hasAccess('read', $this->wiki->GetPageTag())) { $isEntry = $this->getService(EntryManager::class)->isEntry($this->wiki->GetPageTag()); $isList = $this->getService(ListManager::class)->isList($this->wiki->GetPageTag()); diff --git a/includes/services/DuplicationManager.php b/includes/services/DuplicationManager.php index 77b6f69e0..676623f36 100644 --- a/includes/services/DuplicationManager.php +++ b/includes/services/DuplicationManager.php @@ -210,8 +210,8 @@ public function checkPostData($data) if ($data['type'] != 'page' && empty($data['pageTitle'])) { throw new \Exception(_t('EMPTY_PAGE_TITLE')); } - if (!$this->wiki->services->get(AclService::class)->hasAccess('write', $data['pageTag'])) { - throw new \Exception(_t('LOGIN_NOT_AUTORIZED_EDIT') . ' ' . $data['pageTag']); + if (!$this->wiki->UserIsAdmin()) { + throw new \Exception(_t('ONLY_ADMINS_CAN_DUPLICATE') . '.'); } $page = $this->wiki->services->get(PageManager::class)->getOne($data['pageTag']); if ($page) { diff --git a/lang/yeswiki_fr.php b/lang/yeswiki_fr.php index cb6b51c2d..c1c6f27a9 100644 --- a/lang/yeswiki_fr.php +++ b/lang/yeswiki_fr.php @@ -646,4 +646,5 @@ 'WIKI_URL' => 'Url du YesWiki de destination', 'WIKI_URL_RECENT' => 'Entrez l\'url d\'un YesWiki assez récent (version > 4.5) pour recevoir la duplication', 'VERIFY_WIKI' => 'Vérifier cette adresse', + 'ONLY_ADMINS_CAN_DUPLICATE' => 'Seuls les membres du groupe "admins" peuvent dupliquer', ]; From 913acbf92092903fddc356e7f647f17b365f558c Mon Sep 17 00:00:00 2001 From: Florian Schmitt Date: Mon, 13 May 2024 16:22:46 +0300 Subject: [PATCH 09/27] feat(duplication): auth routes and verify url and login for distant duplication --- handlers/DuplicateHandler.php | 8 ++- includes/controllers/AuthController.php | 6 +- includes/services/DuplicationManager.php | 3 + javascripts/handlers/duplicate.js | 82 ++++++++++++++++++++++ lang/yeswiki_fr.php | 7 +- lang/yeswikijs_fr.php | 85 ++++++++++++----------- templates/handlers/duplicate-inner.twig | 56 +++++++++------ templates/handlers/duplicate.twig | 2 - tools/login/actions/LoginAction.php | 4 +- tools/login/controllers/ApiController.php | 73 ++++++++++++++++--- 10 files changed, 241 insertions(+), 85 deletions(-) create mode 100644 javascripts/handlers/duplicate.js diff --git a/handlers/DuplicateHandler.php b/handlers/DuplicateHandler.php index 36add2cf8..c5e195da2 100644 --- a/handlers/DuplicateHandler.php +++ b/handlers/DuplicateHandler.php @@ -21,6 +21,7 @@ public function run() $this->entryController = $this->getService(EntryController::class); $this->duplicationManager = $this->getService(DuplicationManager::class); $output = $title = ''; + $toExternalWiki = isset($_GET['toUrl']) && $_GET['toUrl'] == "1"; if (!$this->wiki->page) { $output = $this->render('@templates\alert-message.twig', [ 'type' => 'warning', @@ -60,7 +61,7 @@ public function run() 'message' => $th->getMessage(), ]); } - } elseif (!$this->wiki->UserIsAdmin()) { + } elseif (!$toExternalWiki && !$this->wiki->UserIsAdmin()) { $output .= $this->render('@templates\alert-message-with-back.twig', [ 'type' => 'warning', 'message' => _t('ONLY_ADMINS_CAN_DUPLICATE') . '.', @@ -95,11 +96,12 @@ public function run() 'pageTitle' => $pageTitle, 'totalSize' => $this->duplicationManager->humanFilesize($totalSize), 'type' => $type, - 'toExternalWiki' => isset($_GET['toUrl']) && $_GET['toUrl'] == "1" + 'baseUrl' => $this->wiki->config['base_url'], + 'toExternalWiki' => $toExternalWiki, ]); } - if (isset($_GET['toUrl']) && $_GET['toUrl'] == "1") { + if ($toExternalWiki) { $title .= ' ' . _t('TO_ANOTHER_YESWIKI'); } // in ajax request for modal, no title diff --git a/includes/controllers/AuthController.php b/includes/controllers/AuthController.php index 2db97a7f7..490c05f08 100644 --- a/includes/controllers/AuthController.php +++ b/includes/controllers/AuthController.php @@ -155,10 +155,12 @@ public function connectUser() // connect in SESSION $this->login($data['user'], $data['remember'] ? 1 : 0); } catch (BadUserConnectException $th) { - if (empty($_SESSION['user']['name']) || + if ( + empty($_SESSION['user']['name']) || empty($data['user']['name']) || $data['user']['name'] != $_SESSION['user']['name'] || - !$this->wiki->UserIsAdmin($data['user']['name'])) { + !$this->wiki->UserIsAdmin($data['user']['name']) + ) { // do not disconnect admin during update $this->logout(); } diff --git a/includes/services/DuplicationManager.php b/includes/services/DuplicationManager.php index 676623f36..fb6fd2337 100644 --- a/includes/services/DuplicationManager.php +++ b/includes/services/DuplicationManager.php @@ -225,6 +225,9 @@ public function checkPostData($data) public function duplicateLocally($data) { + if (!$this->wiki->UserIsAdmin()) { + throw new \Exception(_t('ONLY_ADMINS_CAN_DUPLICATE') . '.'); + } switch ($data['type']) { case 'list': $list = $this->wiki->services->get(ListManager::class)->getOne($this->wiki->getPageTag()); diff --git a/javascripts/handlers/duplicate.js b/javascripts/handlers/duplicate.js new file mode 100644 index 000000000..b675e9752 --- /dev/null +++ b/javascripts/handlers/duplicate.js @@ -0,0 +1,82 @@ +// Define a function that takes a string and returns true if it is a valid URL, false otherwise +function isValidUrl(string) { + try { + const url = new URL(string) + return url + } catch (error) { + return false + } +} + +function handleLoginResponse(data) { + if (data.isAdmin === true) { + $('#login-message').html(`
    + ${_t('CONNECTED_AS_ADMIN', { user: data.user })} +
    `) + $('.login-fields').addClass('hide') + $('.duplication-fields').removeClass('hide') + } else { + $('#login-message').html(`
    + ${_t('CONNECTED_BUT_NOT_ADMIN', { user: data.user })} +
    `) + $('.login-fields').removeClass('hide') + } +} + +document.addEventListener('DOMContentLoaded', () => { + let shortUrl = '' + $('#urlWiki').on('change', () => { + $('.login-fields, .duplication-fields').addClass('hide') + $('#login-message').html('') + }) + + $('.btn-distant-login').on('click', () => { + $.ajax({ + method: 'POST', + url: `${shortUrl}/?api/login`, + data: { + username: $('#username').val(), + password: $('#password').val() + } + }).done((data) => { + handleLoginResponse(data) + }).fail((jqXHR) => { + toastMessage(jqXHR.responseJSON.error, 3000, 'alert alert-danger') + if (jqXHR.status === 401) { + $('#login-message').html(`
    ${_t('NOT_CONNECTED')}
    `) + $('.login-fields').removeClass('hide') + } + }) + }) + + $('.btn-verify-wiki').on('click', () => { + let url = $('#urlWiki').val() + + if (isValidUrl(url)) { + let taburl = [] + if (url.search('wakka.php') > -1) { + taburl = url.split('wakka.php') + } else { + taburl = url.split('?') + } + shortUrl = taburl[0].replace(/\/+$/g, '') + $('#baseUrl').text(`${shortUrl}/?`) + url = `${shortUrl}/?api/auth/me` + $.ajax({ + method: 'GET', + url + }).done((data) => { + handleLoginResponse(data) + }).fail((jqXHR) => { + if (jqXHR.status === 401) { + $('#login-message').html(`
    ${_t('NOT_CONNECTED')}
    `) + $('.login-fields').removeClass('hide') + } else { + toastMessage(_t('NOT_WIKI_OR_OLD_WIKI', { url }), 3000, 'alert alert-danger') + } + }) + } else { + toastMessage(_t('NOT_VALID_URL', { url }), 3000, 'alert alert-danger') + } + }) +}) diff --git a/lang/yeswiki_fr.php b/lang/yeswiki_fr.php index c1c6f27a9..41466e5a5 100644 --- a/lang/yeswiki_fr.php +++ b/lang/yeswiki_fr.php @@ -638,13 +638,14 @@ 'DUPLICATE_AND_EDIT' => 'Dupliquer et éditer', 'NO_DUPLICATE_ACTION' => 'Pas d\'action après duplication indiquée (duplicate-action)', 'PAGE_TITLE_TO_DUPLICATE' => 'Titre après duplication', - 'PAGE_TAG_TO_DUPLICATE' => 'Identifiant après duplication', + 'PAGE_TAG_TO_DUPLICATE' => 'Identifiant de la page dupliquée', 'NO_VALID_DATA_TYPE' => 'Pas de type de données valide', 'EMPTY_PAGE_TAG' => 'L\'identifiant ne peut pas être vide', 'EMPTY_PAGE_TITLE' => 'Le titre ne peut pas être vide', 'TO_ANOTHER_YESWIKI' => 'vers un autre YesWiki', 'WIKI_URL' => 'Url du YesWiki de destination', - 'WIKI_URL_RECENT' => 'Entrez l\'url d\'un YesWiki assez récent (version > 4.5) pour recevoir la duplication', + 'WIKI_URL_RECENT' => 'Entrez l\'url d\'un YesWiki version > 4.5.0 pour que la duplication puisse fonctionner', 'VERIFY_WIKI' => 'Vérifier cette adresse', - 'ONLY_ADMINS_CAN_DUPLICATE' => 'Seuls les membres du groupe "admins" peuvent dupliquer', + 'ONLY_ADMINS_CAN_DUPLICATE' => 'Seuls les membres du groupe "admins" de ce wiki peuvent dupliquer localement', + 'DISTANT_LOGIN' => 'Se connecter sur le YesWiki de destination', ]; diff --git a/lang/yeswikijs_fr.php b/lang/yeswikijs_fr.php index 7d78d6998..ae5c8e367 100644 --- a/lang/yeswikijs_fr.php +++ b/lang/yeswikijs_fr.php @@ -40,50 +40,50 @@ 'YES' => 'Oui', // /javascripts/actions/admin-backups.js - 'ADMIN_BACKUPS_LOADING_LIST' => 'Chargement de la liste des sauvegardes', - 'ADMIN_BACKUPS_NOT_POSSIBLE_TO_LOAD_LIST' => 'Impossible de mettre à jour la liste des sauvegardes', - 'ADMIN_BACKUPS_DELETE_ARCHIVE' => 'Suppression de {filename}', - 'ADMIN_BACKUPS_DELETE_ARCHIVE_POSSIBLE_ERROR' => 'Une erreur pourrait avoir eu lieu en supprimant {filename}', - 'ADMIN_BACKUPS_DELETE_ARCHIVE_SUCCESS' => 'Suppression réussie de {filename}', - 'ADMIN_BACKUPS_DELETE_ARCHIVE_ERROR' => 'Suppression impossible de {filename}', - 'ADMIN_BACKUPS_NO_ARCHIVE_TO_DELETE' => 'Aucune sauvegarde à supprimer', - 'ADMIN_BACKUPS_DELETE_SELECTED_ARCHIVES' => 'Suppression des sauvegardes sélectionnées', - 'ADMIN_BACKUPS_RESTORE_ARCHIVE' => 'Restauration de {filename}', - 'ADMIN_BACKUPS_RESTORE_ARCHIVE_POSSIBLE_ERROR' => 'Une erreur pourrait avoir eu lieu en restraurant {filename}', - 'ADMIN_BACKUPS_RESTORE_ARCHIVE_SUCCESS' => 'Restauration réussie de {filename}', - 'ADMIN_BACKUPS_RESTORE_ARCHIVE_ERROR' => 'Restauration impossible de {filename}', - 'ADMIN_BACKUPS_START_BACKUP' => "Lancement d'une sauvegarde", - 'ADMIN_BACKUPS_START_BACKUP_SYNC' => "Lancement d'une sauvegarde en direct (moins stable)\n" . + "ADMIN_BACKUPS_LOADING_LIST" => "Chargement de la liste des sauvegardes", + "ADMIN_BACKUPS_NOT_POSSIBLE_TO_LOAD_LIST" => "Impossible de mettre à jour la liste des sauvegardes", + "ADMIN_BACKUPS_DELETE_ARCHIVE" => "Suppression de {filename}", + "ADMIN_BACKUPS_DELETE_ARCHIVE_POSSIBLE_ERROR" => "Une erreur pourrait avoir eu lieu en supprimant {filename}", + "ADMIN_BACKUPS_DELETE_ARCHIVE_SUCCESS" => "Suppression réussie de {filename}", + "ADMIN_BACKUPS_DELETE_ARCHIVE_ERROR" => "Suppression impossible de {filename}", + "ADMIN_BACKUPS_NO_ARCHIVE_TO_DELETE" => "Aucune sauvegarde à supprimer", + "ADMIN_BACKUPS_DELETE_SELECTED_ARCHIVES" => "Suppression des sauvegardes sélectionnées", + "ADMIN_BACKUPS_RESTORE_ARCHIVE" => "Restauration de {filename}", + "ADMIN_BACKUPS_RESTORE_ARCHIVE_POSSIBLE_ERROR" => "Une erreur pourrait avoir eu lieu en restraurant {filename}", + "ADMIN_BACKUPS_RESTORE_ARCHIVE_SUCCESS" => "Restauration réussie de {filename}", + "ADMIN_BACKUPS_RESTORE_ARCHIVE_ERROR" => "Restauration impossible de {filename}", + "ADMIN_BACKUPS_START_BACKUP" => "Lancement d'une sauvegarde", + "ADMIN_BACKUPS_START_BACKUP_SYNC" => "Lancement d'une sauvegarde en direct (moins stable)\n" . "Il ne sera pas possible de mettre à jour le statut en direct\n" . - 'Ne pas fermer, ni rafraîchir cette fenêtre !', - 'ADMIN_BACKUPS_STARTED' => 'Sauvegarde lancée', - 'ADMIN_BACKUPS_START_BACKUP_ERROR' => 'Lancement de la sauvegarde impossible', - 'ADMIN_BACKUPS_UPDATE_UID_STATUS_ERROR' => 'Impossible de mettre à jour le statut de la sauvegarde', - 'ADMIN_BACKUPS_UID_STATUS_NOT_FOUND' => "Les informations de suivi n'ont pas été trouvées", - 'ADMIN_BACKUPS_UID_STATUS_RUNNING' => 'Sauvegarde en cours', - 'ADMIN_BACKUPS_UID_STATUS_FINISHED' => 'Sauvegarde terminée', - 'ADMIN_BACKUPS_UID_STATUS_NOT_FINISHED' => "Il y a un problème car la sauvegarde n'est plus en cours et elle n'est pas terminée !", - 'ADMIN_BACKUPS_UID_STATUS_STOP' => 'Sauvegarde arrêtée', - 'ADMIN_BACKUPS_STOP_BACKUP_ERROR' => "Erreur : impossible d'arrêter la sauvegarde", - 'ADMIN_BACKUPS_STOPPING_ARCHIVE' => 'Arrêt en cours de la sauvegarde', - 'ADMIN_BACKUPS_CONFIRMATION_TO_DELETE' => "Les fichiers suivants seront supprimés par la sauvegarde.\n" . + "Ne pas fermer, ni rafraîchir cette fenêtre !", + "ADMIN_BACKUPS_STARTED" => "Sauvegarde lancée", + "ADMIN_BACKUPS_START_BACKUP_ERROR" => "Lancement de la sauvegarde impossible", + "ADMIN_BACKUPS_UPDATE_UID_STATUS_ERROR" => "Impossible de mettre à jour le statut de la sauvegarde", + "ADMIN_BACKUPS_UID_STATUS_NOT_FOUND" => "Les informations de suivi n'ont pas été trouvées", + "ADMIN_BACKUPS_UID_STATUS_RUNNING" => "Sauvegarde en cours", + "ADMIN_BACKUPS_UID_STATUS_FINISHED" => "Sauvegarde terminée", + "ADMIN_BACKUPS_UID_STATUS_NOT_FINISHED" => "Il y a un problème car la sauvegarde n'est plus en cours et elle n'est pas terminée !", + "ADMIN_BACKUPS_UID_STATUS_STOP" => "Sauvegarde arrêtée", + "ADMIN_BACKUPS_STOP_BACKUP_ERROR" => "Erreur : impossible d'arrêter la sauvegarde", + "ADMIN_BACKUPS_STOPPING_ARCHIVE" => "Arrêt en cours de la sauvegarde", + "ADMIN_BACKUPS_CONFIRMATION_TO_DELETE" => "Les fichiers suivants seront supprimés par la sauvegarde.\n" . "Veuillez confirmer leur suppression en cochant la case ci-dessous.\n
    {files}
    ", - 'ADMIN_BACKUPS_START_BACKUP_ERROR_ARCHIVING' => "Lancement de la sauvegarde impossible car une sauvegarde semble être déjà en cours.\n" . - 'Suivez
    cette aide pour corriger ceci.', - 'ADMIN_BACKUPS_START_BACKUP_ERROR_HIBERNATE' => "Lancement de la sauvegarde impossible car le site est en hibernation.\n" . + "ADMIN_BACKUPS_START_BACKUP_ERROR_ARCHIVING" => "Lancement de la sauvegarde impossible car une sauvegarde semble être déjà en cours.\n" . + "Suivez cette aide pour corriger ceci.", + "ADMIN_BACKUPS_START_BACKUP_ERROR_HIBERNATE" => "Lancement de la sauvegarde impossible car le site est en hibernation.\n" . "Suivez cette aide pour en sortir.", - 'ADMIN_BACKUPS_START_BACKUP_PATH_NOT_WRITABLE' => "Lancement de la sauvegarde impossible car le dossier de sauvegarde n'est pas accessible en écriture.\n" . - 'Suivez cette aide pour configurer le dossier de sauvegarde.', - 'ADMIN_BACKUPS_FORCED_UPDATE_NOT_POSSIBLE' => 'Mise à jour forcée impossible', - 'ADMIN_BACKUPS_UID_STATUS_FINISHED_THEN_UPDATING' => 'Mise à jour lancée (veuillez patienter)', - 'ADMIN_BACKUPS_START_BACKUP_CANNOT_EXEC' => "Lancement de la sauvegarde impossible \n" . + "ADMIN_BACKUPS_START_BACKUP_PATH_NOT_WRITABLE" => "Lancement de la sauvegarde impossible car le dossier de sauvegarde n'est pas accessible en écriture.\n" . + "Suivez cette aide pour configurer le dossier de sauvegarde.", + "ADMIN_BACKUPS_FORCED_UPDATE_NOT_POSSIBLE" => "Mise à jour forcée impossible", + "ADMIN_BACKUPS_UID_STATUS_FINISHED_THEN_UPDATING" => "Mise à jour lancée (veuillez patienter)", + "ADMIN_BACKUPS_START_BACKUP_CANNOT_EXEC" => "Lancement de la sauvegarde impossible \n" . "Car il n'est pas possible de lancer des commandes console sur le serveur " . - '(voir aide).', - 'ADMIN_BACKUPS_START_BACKUP_FOLDER_AVAILABLE' => "Lancement de la sauvegarde impossible car le dossier de sauvegarde est accessible sur internet.\n" . + "(voir aide).", + "ADMIN_BACKUPS_START_BACKUP_FOLDER_AVAILABLE" => "Lancement de la sauvegarde impossible car le dossier de sauvegarde est accessible sur internet.\n" . "Suivez cette aide pour restreindre l'accès au dossier de sauvegarde.", - 'ADMIN_BACKUPS_START_BACKUP_NOT_ENOUGH_SPACE' => "Lancement de la sauvegarde impossible \n" . + "ADMIN_BACKUPS_START_BACKUP_NOT_ENOUGH_SPACE" => "Lancement de la sauvegarde impossible \n" . "Il n'y a plus assez d'espace disque disponible pour une nouvelle sauvegarde.", - 'ADMIN_BACKUPS_START_BACKUP_NOT_DB' => "Lancement de la sauvegarde non optimal \n" . + "ADMIN_BACKUPS_START_BACKUP_NOT_DB" => "Lancement de la sauvegarde non optimal \n" . "L'utilitaire d'export de base de données ('mysqldump') n'est pas accessible (voir aide).", // /javascripts/handlers/revisions.js @@ -149,5 +149,12 @@ 'REACTION_CONFIRM_DELETE_ALL' => 'Etes-vous sur de vouloir supprimer toutes les réactions de ce vote ?', // Doc - 'DOC_EDIT_THIS_PAGE_ON_GITHUB' => 'Modifier cette page sur Github', + "DOC_EDIT_THIS_PAGE_ON_GITHUB" => "Modifier cette page sur Github", + + // Duplication + 'NOT_VALID_URL' => 'Url non valide : {url}', + 'NOT_CONNECTED' => 'Il faut se connecter au YesWiki de destination avec un compte disposant de droits d\'administration.', + 'CONNECTED_AS_ADMIN' => 'L\'utilisateurice {user} est bien connecté.e sur le YesWiki de destination, avec des droits d\'administration.', + 'CONNECTED_BUT_NOT_ADMIN' => 'L\'utilisateurice {user} est bien connecté.e sur le YesWiki de destination, mais n\'a pas les droits d\'administration. Veuillez-vous connecter avec un compte admin.', + 'NOT_WIKI_OR_OLD_WIKI' => 'Le site indiqué ne semble pas être un YesWiki, ou sa version est antérieure à 4.5.0', ]; diff --git a/templates/handlers/duplicate-inner.twig b/templates/handlers/duplicate-inner.twig index 24f27c197..2a01afb40 100644 --- a/templates/handlers/duplicate-inner.twig +++ b/templates/handlers/duplicate-inner.twig @@ -1,40 +1,50 @@ -
    - {% if toExternalWiki %} -
    {{ _t('WIKI_URL_RECENT') }}.
    +{% if toExternalWiki %} + {{ include_javascript('javascripts/handlers/duplicate.js') }} +
    {{ _t('WIKI_URL_RECENT') }}.
    -
    -
    - - - - -
    -
    +
    + + + + +
    -