Compare commits

..

4 Commits

8 changed files with 428 additions and 19 deletions

View File

@@ -2,8 +2,8 @@
-- PostgreSQL database dump -- PostgreSQL database dump
-- --
-- Dumped from database version 18.1 -- Dumped from database version 18.3
-- Dumped by pg_dump version 18.1 -- Dumped by pg_dump version 18.3
SET statement_timeout = 0; SET statement_timeout = 0;
SET lock_timeout = 0; SET lock_timeout = 0;
@@ -86,6 +86,29 @@ CREATE TYPE public.user_rank_enum AS ENUM (
ALTER TYPE public.user_rank_enum OWNER TO kabano; ALTER TYPE public.user_rank_enum OWNER TO kabano;
--
-- Name: content_comment_photos; Type: TABLE; Schema: public; Owner: kabano
--
CREATE SEQUENCE public.content_comment_photos_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.content_comment_photos_id_seq OWNER TO kabano;
CREATE TABLE public.content_comment_photos (
id integer NOT NULL,
comment_id integer NOT NULL,
url text NOT NULL,
creation_date timestamp without time zone DEFAULT now()
);
ALTER TABLE public.content_comment_photos OWNER TO kabano;
-- --
-- Name: content_comments_sequence; Type: SEQUENCE; Schema: public; Owner: kabano -- Name: content_comments_sequence; Type: SEQUENCE; Schema: public; Owner: kabano
-- --
@@ -407,6 +430,19 @@ CREATE TABLE public.users (
ALTER TABLE public.users OWNER TO kabano; ALTER TABLE public.users OWNER TO kabano;
--
-- Name: content_comment_photos id; Type: DEFAULT; Schema: public; Owner: kabano
--
ALTER TABLE ONLY public.content_comment_photos ALTER COLUMN id SET DEFAULT nextval('public.content_comment_photos_id_seq'::regclass);
--
-- Name: content_comment_photos content_comment_photos_pkey; Type: CONSTRAINT; Schema: public; Owner: kabano
--
ALTER TABLE ONLY public.content_comment_photos
ADD CONSTRAINT content_comment_photos_pkey PRIMARY KEY (id);
-- --
-- Data for Name: locales; Type: TABLE DATA; Schema: public; Owner: kabano -- Data for Name: locales; Type: TABLE DATA; Schema: public; Owner: kabano
@@ -625,6 +661,14 @@ CREATE INDEX users_is_archive_index ON public.users USING btree (is_archive);
CREATE INDEX users_register_date_index ON public.users USING btree (register_date); CREATE INDEX users_register_date_index ON public.users USING btree (register_date);
--
-- Name: content_comment_photos content_comment_photos_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: kabano
--
ALTER TABLE ONLY public.content_comment_photos
ADD CONSTRAINT content_comment_photos_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES public.content_comments(id) ON DELETE CASCADE;
-- --
-- Name: content_comments content_comments_author_fkey; Type: FK CONSTRAINT; Schema: public; Owner: kabano -- Name: content_comments content_comments_author_fkey; Type: FK CONSTRAINT; Schema: public; Owner: kabano
-- --

BIN
public/medias/watermark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -99,7 +99,7 @@ section.archive > * {
#new_comment_form #photo { #new_comment_form #photo {
display: none; display: none;
} }
#new_comment_form .upload-btn { #new_comment_form #upload-btn {
display: block; display: block;
width: 100%; width: 100%;
border-bottom: 2px solid blue; border-bottom: 2px solid blue;

View File

@@ -23,6 +23,7 @@
<a href="<?=$config['rel_root_folder']?>admin/logs" class="button"><i class="fas fa-history"></i> Voir les logs</a> <small>Permet d'accéder aux 200 dernières lignes des logs bruts des actions sur la base de données.</small><br><br> <a href="<?=$config['rel_root_folder']?>admin/logs" class="button"><i class="fas fa-history"></i> Voir les logs</a> <small>Permet d'accéder aux 200 dernières lignes des logs bruts des actions sur la base de données.</small><br><br>
<a href="<?=$config['rel_root_folder']?>admin/wiki-files" class="button"><i class="fas fa-paperclip"></i> Fichiers attachés</a><small>Gérer les fichiers attachés pour le wiki : liste, ajout, suppression...</small><br><br> <a href="<?=$config['rel_root_folder']?>admin/wiki-files" class="button"><i class="fas fa-paperclip"></i> Fichiers attachés</a><small>Gérer les fichiers attachés pour le wiki : liste, ajout, suppression...</small><br><br>
<a href="<?=$config['rel_root_folder']?>admin/stats" class="button"><i class="fas fa-chart-line"></i> Statistiques</a><small>Analyser les logs et afficher les statistiques.</small><br><br> <a href="<?=$config['rel_root_folder']?>admin/stats" class="button"><i class="fas fa-chart-line"></i> Statistiques</a><small>Analyser les logs et afficher les statistiques.</small><br><br>
<a href="<?=$config['rel_root_folder']?>admin/wri-import" class="button"><i class="fas fa-cloud-download-alt"></i> Import WRI</a><small>Importe les points de Refuges.info.</small><br><br>
<?php } ?> <?php } ?>
</section> </section>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<!-- Page: admin logs -->
<html lang="fr">
<?php include('blocks/d.head.html'); ?>
<body>
<?php include('blocks/d.nav.html'); ?>
<section>
<h1><?=$head['title']?></h1>
<p>
Limport depuis <strong>Refuges.info</strong> vient dêtre exécuté.
</p>
<h2>Résumé</h2>
<ul>
<li><strong>Nouveaux POIs créés :</strong> <?= $result['created'] ?></li>
<li><strong>POIs mis à jour :</strong> <?= $result['updated'] ?></li>
<li><strong>Total analysés :</strong> <?= $result['total'] ?></li>
</ul>
</section>
<?php include('blocks/d.footer.html'); ?>
</body>
</html>

View File

@@ -178,11 +178,11 @@
</div> </div>
<?php if ($isCommentable) { ?> <?php if ($isCommentable && $poi->source_id=='kab') { ?>
<?php if (isset($poi_comments) && $poi_comments->number > 0) { ?> <?php if (isset($poi_comments) && $poi_comments->number > 0) { ?>
<div id="comments_photos_gallery" class="gallery"> <div id="comments_photos_gallery" class="gallery">
<?php foreach ($poi_comments->objs as $comment) { ?> <?php foreach ($poi_comments->objs as $comment) { ?>
<?php if (!empty($comment->photo)) { ?> <?php if (!empty($comment->photo) && $comment->is_archive != 't' && $comment->is_public != 'f') { ?>
<a href="<?=$config['rel_root_folder'].'medias/comment_photos/'.$comment->photo?>"> <a href="<?=$config['rel_root_folder'].'medias/comment_photos/'.$comment->photo?>">
<img src="<?=$config['rel_root_folder'].'medias/comment_photos/'.$comment->photo?>" title="<?= (mb_strlen($comment->comment) > 50 ? mb_substr($comment->comment, 0, 50) . '...' : $comment->comment) ?>"> <img src="<?=$config['rel_root_folder'].'medias/comment_photos/'.$comment->photo?>" title="<?= (mb_strlen($comment->comment) > 50 ? mb_substr($comment->comment, 0, 50) . '...' : $comment->comment) ?>">
</a> </a>
@@ -211,7 +211,7 @@
<div id="new_comment_form"> <div id="new_comment_form">
<textarea id="comment" name="comment" rows="5" placeholder="Votre commentaire"></textarea> <textarea id="comment" name="comment" rows="5" placeholder="Votre commentaire"></textarea>
<label for="photo" class="upload-btn"> <label for="photo" id="upload-btn">
<i class="fas fa-file-image"></i> Ajouter une photo <i class="fas fa-file-image"></i> Ajouter une photo
</label> </label>
<input type="file" id="photo" name="photo" accept="image/*"> <input type="file" id="photo" name="photo" accept="image/*">
@@ -219,6 +219,14 @@
</form> </form>
</div> </div>
<script>
document.getElementById('photo').addEventListener('change', function() {
document.getElementById('upload-btn').innerHTML = this.files.length
? "<i class='fas fa-check-square'></i> Photo sélectionnée"
: "<i class='fas fa-file-image'></i> Ajouter une photo";
});
</script>
<?php if (isset($poi_comments) && $poi_comments->number > 0) { ?> <?php if (isset($poi_comments) && $poi_comments->number > 0) { ?>
<!-- Comment list --> <!-- Comment list -->
<div id="comments"> <div id="comments">
@@ -279,6 +287,8 @@
<?php } ?> <?php } ?>
<?php } ?> <?php } ?>
<?php if($poi->source_id!='kab') {?><br><div style="text-align: center; font-style: italic;">Données fournies par <a href="https://refuges.info/point/<?=$poi->remote_source_id?>" target="_blank"><i class="fas fa-external-link-alt"></i> <?=$poi->source?></a> sous licence CC BY-SA</div><?php } ?>
</section> </section>
<?php include('blocks/d.footer.html'); ?> <?php include('blocks/d.footer.html'); ?>

View File

@@ -229,31 +229,33 @@ if(isset($controller->splitted_url[1]) && $user->rankIsHigher("moderator")) {
$output = Array(); $output = Array();
$backup_file = Array(); $backup_file = Array();
// Suppression d'une archive existante. // Suppression d'une archive existante.
if(isset($controller->splitted_url[2]) && $controller->splitted_url[2]=='delete' && isset($controller->splitted_url[3])) { if(isset($controller->splitted_url[2]) && $controller->splitted_url[2]=='delete' && isset($controller->splitted_url[3])) {
$tmp_folder = realpath($config['public_folder'].'tmp'); $tmp_folder = realpath($config['public_folder'].'tmp');
if ($tmp_folder !== false) { if ($tmp_folder !== false) {
$safe_name = basename($controller->splitted_url[3]); $safe_name = basename($controller->splitted_url[3]);
$tmp_folder_root = rtrim($tmp_folder, DIRECTORY_SEPARATOR); $tmp_folder_root = rtrim($tmp_folder, DIRECTORY_SEPARATOR);
$delete_path = $tmp_folder_root . DIRECTORY_SEPARATOR . $safe_name; $delete_path = $tmp_folder_root . DIRECTORY_SEPARATOR . $safe_name;
$real_delete_path = realpath($delete_path); $real_delete_path = realpath($delete_path);
if ($real_delete_path && str_starts_with($real_delete_path, $tmp_folder_root . DIRECTORY_SEPARATOR)) { if ($real_delete_path && str_starts_with($real_delete_path, $tmp_folder_root . DIRECTORY_SEPARATOR)) {
if (file_exists($real_delete_path)) { if (file_exists($real_delete_path)) {
unlink($real_delete_path); unlink($real_delete_path);
}
} }
} }
} }
}
else { else {
// Création des archives de fichiers. // Création des archives de fichiers.
// Nom du fichier de sauvegarde // Nom du fichier de sauvegarde
$timestamp = date('Ymd_His'); $timestamp = date('Ymd_His');
$backup_source[0] = $config['public_folder'].'medias/avatars'; $backup_source[0] = $config['public_folder'].'medias/avatars';
$backup_source[1] = $config['public_folder'].'medias/wiki'; $backup_source[1] = $config['public_folder'].'medias/wiki';
$backup_source[2] = $config['public_folder'].'medias/comment_photos';
$backup_filename[0] = $timestamp.'_avatar_files.zip'; $backup_filename[0] = $timestamp.'_avatar_files.zip';
$backup_filename[1] = $timestamp.'_wiki_files.zip'; $backup_filename[1] = $timestamp.'_wiki_files.zip';
$backup_filename[2] = $timestamp.'_comment_photos.zip';
for($i=0;$i<2;$i++) { for($i=0;$i<3;$i++) {
$backup_file[$i] = $config['public_folder'].'tmp/'.$backup_filename[$i]; $backup_file[$i] = $config['public_folder'].'tmp/'.$backup_filename[$i];
$backup[$i] = new ZipArchive(); $backup[$i] = new ZipArchive();
@@ -328,6 +330,22 @@ if(isset($controller->splitted_url[1]) && $user->rankIsHigher("moderator")) {
$notfound = 1; $notfound = 1;
} }
break; break;
case 'wri-import':
if ($user->rankIsHigher("moderator")) {
require_once($config['abs_root_folder']."src/Import/wri.php");
$head['title'] = "Import Refuges.info";
$importer = new \Kabano\Import\WriImporter();
$result = $importer->importAll(false);
include ($config['views_folder']."d.admin.wri-import.html");
}
else {
$notfound = 1;
}
break;
default: default:
$notfound = 1; $notfound = 1;
break; break;

306
src/Import/wri.php Normal file
View File

@@ -0,0 +1,306 @@
<?php
namespace Kabano\Import;
use Kabano\Poi;
use Exception;
require_once($config['models_folder']."d.poi.php");
require_once($config['includes_folder']."poi_types.struct.php");
class WriFetcher
{
private string $url;
public function __construct(
string $url = 'https://www.refuges.info/api/bbox?detail=complet&nb_points=all'
) {
$this->url = $url;
}
public function fetchAll(): array
{
$json = $this->download($this->url);
$data = json_decode($json, true);
if ($data === null) {
throw new Exception(
'JSON invalide reçu depuis refuges.info : ' . json_last_error_msg()
);
}
return $data;
}
private function download(string $url): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_USERAGENT => 'KabanoBot/1.0 (+https://kabano.org)',
]);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
throw new Exception("Erreur réseau lors du téléchargement : $error");
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status < 200 || $status >= 300) {
throw new Exception("HTTP $status reçu depuis $url");
}
return $response;
}
}
class WriMapper
{
private array $poi_types;
public function __construct()
{
global $poi_types;
$this->poi_types = $poi_types;
}
public function map(array $raw): Poi
{
if (!isset($raw['properties']) || !isset($raw['geometry'])) {
throw new Exception("Feature GeoJSON invalide");
}
$p = $raw['properties'];
$g = $raw['geometry'];
// Type WRI (structure confirmée)
$wriTypeId = $p['type']['id'] ?? null;
// Filtrage strict
$poiType = $this->mapTypeFromWriId((int)$wriTypeId);
if ($poiType === null) {
throw new Exception("Type WRI non supporté : $wriTypeId");
}
// Si WRI indique "manque un mur", on force le type en basic_hut
if (($p['info_comp']['manque_un_mur']['valeur'] ?? '') === 'Oui') {
$poiType = 'basic_hut';
}
$poi = new Poi();
$poi->source_id = 'wri';
$poi->remote_source_id = $p['id'];
$poi->name = $p['nom'] ?? '—';
$poi->permalink = $this->slugify($poi->name);
$poi->poi_type = $poiType;
// Coordonnées
$poi->lat = $g['coordinates'][1] ?? null;
$poi->lon = $g['coordinates'][0] ?? null;
$poi->ele = $p['coord']['alt'] ?? 0;
$poi->locale = 'fr_FR';
$poi->is_commentable = true;
// Paramètres JSON conformes à ton modèle
$poi->parameters = json_encode(
$this->buildParameters($poiType, $p),
JSON_UNESCAPED_UNICODE
);
return $poi;
}
private function mapTypeFromWriId(int $id): ?string
{
return match ($id) {
10 => 'alpine_hut', // refuge gardé
7 => 'wilderness_hut', // cabane non gardée
9 => 'halt', // gîte d'étape
default => null
};
}
private function cleanValue($value): string
{
if (!is_string($value)) return '';
$value = strip_tags(str_replace(['[b]', '[/b]'], '', $value));
return trim($value);
}
private function isYes($value): int
{
// Nettoyage des balises WRI
$clean = strip_tags(str_replace(['[b]', '[/b]'], '', (string)$value));
$clean = trim($clean);
return match ($clean) {
'Oui' => 2,
'Non' => 0,
'Inconnu' => 1,
default => -1
};
}
private function buildParameters(string $poi_type, array $p): array
{
$fields = $this->poi_types[$poi_type][5];
$params = [];
foreach ($fields as $key => $label) {
switch (true) {
// TEXTES
case str_starts_with($key, 't_'):
$params[$key] = match ($key) {
't_owner' => $p['proprio']['valeur'] ?? '',
't_access' => $p['acces']['valeur'] ?? '',
't_description' => $p['remarque']['valeur'] ?? '',
default => '',
};
break;
// BOOLÉENS
case str_starts_with($key, 'b_'):
$params[$key] = $this->guessBoolean($key, $p);
break;
// NUMÉRIQUES
case str_starts_with($key, 'n_'):
$params[$key] = $this->guessNumeric($key, $p);
break;
// LIENS
case str_starts_with($key, 'l_'):
$params[$key] = null; // Non géré par WRI
break;
default:
$params[$key] = "";
}
}
return $params;
}
private function guessBoolean(string $key, array $p): int
{
$etatId = $p['etat']['id'] ?? '';
return match ($key) {
'b_usable' =>
in_array($etatId, ['detruit', 'fermeture'], true) ? 2 : 0,
'b_water' => $this->isYes($p['info_comp']['eau']['valeur'] ?? ''),
'b_wood' => $this->isYes($p['info_comp']['bois']['valeur'] ?? ''),
'b_cover' => $this->isYes($p['info_comp']['couvertures']['valeur'] ?? ''),
'b_toilet' => $this->isYes($p['info_comp']['latrines']['valeur'] ?? ''),
'b_fireplace' => max(
$this->isYes($p['info_comp']['cheminee']['valeur'] ?? ''),
$this->isYes($p['info_comp']['poele']['valeur'] ?? '')
),
'b_key' =>
$etatId === 'cle_a_recuperer' ? 2 : 0,
default => -1,
};
}
private function guessNumeric(string $key, array $p): ?int
{
$raw = match ($key) {
'n_bed' => $p['places']['valeur'] ?? null,
'n_mattress' => $p['info_comp']['places_matelas']['valeur'] ?? null,
'n_bed_winter' => null, // WRI ne fournit pas
default => null,
};
if (!is_numeric($raw)) {
return null;
}
return (int)$raw;
}
private function slugify(string $text): string
{
$text = iconv('UTF-8', 'ASCII//TRANSLIT', $text);
$text = preg_replace('/[^a-zA-Z0-9]+/', '-', $text);
return strtolower(trim($text, '-'));
}
}
class WriImporter
{
private WriFetcher $fetcher;
private WriMapper $mapper;
public function __construct()
{
$this->fetcher = new WriFetcher();
$this->mapper = new WriMapper();
}
public function importAll(bool $dryRun = false): array
{
$geojson = $this->fetcher->fetchAll();
if (!isset($geojson['features']) || !is_array($geojson['features'])) {
throw new Exception("Format GeoJSON inattendu");
}
$raws = $geojson['features'];
$created = 0;
$updated = 0;
foreach ($raws as $raw) {
try {
$poi = $this->mapper->map($raw);
} catch (\Exception $e) {
continue; // Type non supporté
}
// Vérifier si un POI existe déjà
$existing = new Poi();
if ($existing->checkPermalink($poi->permalink, 1)) {
if (!$dryRun) {
$existing->lat = $poi->lat;
$existing->lon = $poi->lon;
$existing->ele = $poi->ele;
$existing->parameters = $poi->parameters;
$existing->poi_type = $poi->poi_type;
$existing->source_id = 'wri';
$existing->remote_source_id = $poi->remote_source_id;
$existing->update();
}
$updated++;
} else {
if (!$dryRun) {
$poi->insert();
}
$created++;
}
}
return [
'created' => $created,
'updated' => $updated,
'total' => count($raws),
];
}
}