ensureGeoExtraField()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
c 0
b 0
f 0
nc 4
nop 3
dl 0
loc 20
rs 9.7666
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
/**
5
 * Social map with user geolocation
6
 * - Checks Google Maps plugin enabled + configured for current access URL
7
 * - Reads API key and extra field list from access_url_rel_plugin.configuration
8
 * - Accepts values as JSON {"lat","lng"}, "label::lat,lng" or "lat,lng"
9
 * - LEFT JOIN so a user appears if at least one field has coords
10
 * - Caches result set (APCu -> filesystem)
11
 */
12
13
use Chamilo\CoreBundle\Framework\Container;
14
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
15
use Chamilo\CoreBundle\Helpers\PluginHelper;
16
use Chamilo\CoreBundle\Repository\AccessUrlRelPluginRepository;
17
use Symfony\Component\Cache\Adapter\ApcuAdapter;
18
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
19
20
$cidReset = true;
21
require_once __DIR__.'/../inc/global.inc.php';
22
23
api_block_anonymous_users();
24
25
/* ----------------------- helpers ----------------------- */
26
function extractLatLng($raw) {
27
    if (empty($raw)) return [null, null];
28
    $raw = trim((string) $raw);
29
30
    // JSON {"lat":...,"lng":...}
31
    if (strlen($raw) > 1 && $raw[0] === '{') {
32
        $obj = json_decode($raw, true);
33
        if (json_last_error() === JSON_ERROR_NONE && isset($obj['lat'], $obj['lng'])) {
34
            return [$obj['lat'], $obj['lng']];
35
        }
36
    }
37
    // Legacy "label::lat,lng"
38
    if (strpos($raw, '::') !== false) {
39
        [, $coords] = explode('::', $raw, 2);
40
        $p = array_map('trim', explode(',', $coords, 2));
41
        if (count($p) === 2) return [$p[0], $p[1]];
42
    }
43
    // Simple "lat,lng"
44
    if (strpos($raw, ',') !== false) {
45
        $p = array_map('trim', explode(',', $raw, 2));
46
        if (count($p) === 2) return [$p[0], $p[1]];
47
    }
48
    return [null, null];
49
}
50
51
function resolveGeoType(): int {
52
    if (defined('ExtraField::FIELD_TYPE_GEOLOCALIZATION')) return constant('ExtraField::FIELD_TYPE_GEOLOCALIZATION');
53
    if (defined('ExtraField::FIELD_TYPE_GEOLOCATION'))   return constant('ExtraField::FIELD_TYPE_GEOLOCATION');
54
    if (defined('ExtraField::FIELD_TYPE_TEXT'))          return constant('ExtraField::FIELD_TYPE_TEXT');
55
    return 1; // TEXT fallback
56
}
57
58
function ensureGeoExtraField(ExtraField $ef, ?string $var, string $label): ?array {
59
    if (empty($var)) return null;
60
    $info = $ef->get_handler_field_info_by_field_variable($var);
61
    if (!empty($info)) return $info;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $info could return the type boolean which is incompatible with the type-hinted return array|null. Consider adding an additional type-check to rule them out.
Loading history...
62
63
    // Create the field only for platform admins
64
    if (api_is_platform_admin()) {
65
        $payload = [
66
            'variable'           => $var,
67
            'display_text'       => $label,
68
            'value_type'         => resolveGeoType(),
69
            'visible_to_self'    => 1,
70
            'visible_to_others'  => 1,
71
            'changeable'         => 1,
72
            'created_at'         => date('Y-m-d H:i:s'),
73
        ];
74
        $ef->save($payload);
75
        return $ef->get_handler_field_info_by_field_variable($var);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ef->get_handler_...by_field_variable($var) could return the type boolean which is incompatible with the type-hinted return array|null. Consider adding an additional type-check to rule them out.
Loading history...
76
    }
77
    return null;
78
}
79
80
/* ----------------------- services ----------------------- */
81
$pluginHelper    = Container::$container->get(PluginHelper::class);
82
$accessUrlHelper = Container::$container->get(AccessUrlHelper::class);
83
$pluginRepo      = Container::$container->get(AccessUrlRelPluginRepository::class);
84
85
/* ---------------- validate plugin enabled --------------- */
86
$PLUGIN_NAME = 'google_maps';
87
if (!$pluginHelper->isPluginEnabled($PLUGIN_NAME)) {
88
    if (api_is_platform_admin()) {
89
        Display::display_header(get_lang('Social'));
90
        echo Display::return_message('Google Maps plugin is not enabled for this portal URL. Enable it in Administration → Plugins.', 'warning');
91
        echo '<p><a href="'.api_get_path(WEB_CODE_PATH).'admin/plugins.php">Open Plugins admin</a></p>';
92
        Display::display_footer();
93
    } else {
94
        api_not_allowed(true);
95
    }
96
    exit;
97
}
98
99
/* ---------------- load plugin config for URL ------------ */
100
$currentUrl = $accessUrlHelper->getCurrent();
101
if ($currentUrl === null) {
102
    api_not_allowed(true);
103
}
104
$rel = $pluginRepo->findOneByPluginName($PLUGIN_NAME, $currentUrl->getId());
105
if (!$rel || !$rel->isActive()) {
106
    api_not_allowed(true);
107
}
108
$config = $rel->getConfiguration();
109
if (is_string($config)) {
110
    $tmp = json_decode($config, true);
111
    if (json_last_error() === JSON_ERROR_NONE) $config = $tmp;
112
}
113
if (!is_array($config)) $config = [];
114
115
$enabledRaw = $config['enable_api'] ?? null;
116
$apiKey     = trim((string)($config['api_key'] ?? ''));
117
118
/* truthy parsing for enable_api */
119
$localization = ($enabledRaw === true) || ($enabledRaw === 1) || ($enabledRaw === '1') || ($enabledRaw === 'true') || ($enabledRaw === 'on') || ($enabledRaw === 'yes');
120
121
if (!$localization || $apiKey === '') {
122
    if (api_is_platform_admin()) {
123
        Display::display_header(get_lang('Social'));
124
        echo Display::return_message('Google Maps plugin not configured. Turn on API and set API key in Administration → Plugins → Google Maps.', 'warning');
125
        echo '<p><a href="'.api_get_path(WEB_CODE_PATH).'admin/plugins.php">'.get_lang('Back to plugins').'</a></p>';
126
        Display::display_footer();
127
    } else {
128
        api_not_allowed(true);
129
    }
130
    exit;
131
}
132
133
/* ---------------- fields to use ------------------------ */
134
$pluginFieldsCsv = (string)($config['extra_field_name'] ?? '');
135
$vars = array_values(array_filter(array_map('trim', explode(',', $pluginFieldsCsv))));
136
if (empty($vars)) {
137
    $fieldsSetting = api_get_setting('profile.allow_social_map_fields', true);
138
    if (!$fieldsSetting || empty($fieldsSetting['fields']) || !is_array($fieldsSetting['fields'])) {
139
        api_not_allowed(true);
140
    }
141
    $vars = array_values($fieldsSetting['fields']);
142
}
143
$vars = array_values(array_unique(array_filter($vars)));
144
$var1 = $vars[0] ?? null;
145
$var2 = $vars[1] ?? null;
146
147
/* ---------------- ensure extrafields exist -------------- */
148
$extraField = new ExtraField('user');
149
$info1 = ensureGeoExtraField($extraField, $var1, $var1 ?: 'Geolocation A');
150
$info2 = ensureGeoExtraField($extraField, $var2, $var2 ?: 'Geolocation B');
151
if (empty($info1) && empty($info2)) {
152
    api_not_allowed(true);
153
}
154
155
/* ---------------- build query --------------------------- */
156
$tableUser = Database::get_main_table(TABLE_MAIN_USER);
157
158
$select = "u.id, u.firstname, u.lastname";
159
$joins  = [];
160
$conds  = [];
161
162
if (!empty($info1)) {
163
    $select .= ", ev1.field_value AS f1";
164
    $joins[] = "LEFT JOIN extra_field_values ev1
165
                  ON ev1.item_id = u.id
166
                 AND ev1.field_id = ".$info1['id'];
167
    $conds[] = "COALESCE(ev1.field_value,'') <> ''";
168
}
169
if (!empty($info2)) {
170
    $select .= ", ev2.field_value AS f2";
171
    $joins[] = "LEFT JOIN extra_field_values ev2
172
                  ON ev2.item_id = u.id
173
                 AND ev2.field_id = ".$info2['id'];
174
    $conds[] = "COALESCE(ev2.field_value,'') <> ''";
175
}
176
177
if (empty($conds)) {
178
    api_not_allowed(true);
179
}
180
181
$sql = "SELECT $select
182
        FROM $tableUser u
183
        ".implode("\n", $joins)."
184
        WHERE u.active = 1
185
          AND (".implode(' OR ', $conds).")";
186
187
/* ---------------- caching (APCu -> FS) ------------------ */
188
$useApcu = function_exists('apcu_enabled') ? apcu_enabled() : (extension_loaded('apcu') && (PHP_SAPI !== 'cli' || (bool)ini_get('apc.enable_cli')));
189
190
$cache = $useApcu
191
    ? new ApcuAdapter('social_map')
192
    : new FilesystemAdapter('social_map', 300, rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'chamilo_cache');
193
194
$cacheKey = sprintf('places:url%d:f1_%s:f2_%s', (int)$currentUrl->getId(), (string)($info1['id'] ?? 0), (string)($info2['id'] ?? 0));
195
$item = $cache->getItem($cacheKey);
196
if (!$item->isHit()) {
197
    $result = Database::query($sql);
198
    $data   = Database::store_result($result, 'ASSOC');
199
    $item->set($data);
200
    $item->expiresAfter(300);
201
    $cache->save($item);
202
} else {
203
    $data = $item->get();
204
}
205
206
/* ---------------- massage rows -------------------------- */
207
$guessType = static function (?string $var): ?string {
208
    if (!$var) return null;
209
    $v = strtolower($var);
210
    if (str_contains($v, 'villedustage')) return 'stage';
211
    if (str_contains($v, 'ville')) return 'ville';
212
    return null;
213
};
214
$type1 = $guessType($var1);
215
$type2 = $guessType($var2);
216
217
foreach ($data as &$row) {
218
    $row['complete_name'] = trim($row['firstname'].' '.$row['lastname']);
219
    $row['lastname']  = '';
220
    $row['firstname'] = '';
221
222
    if (array_key_exists('f1', $row)) {
223
        [$aLat, $aLng] = extractLatLng($row['f1']);
224
        if ($aLat !== null && $aLng !== null) {
225
            $row['f1_lat']  = $aLat;
226
            $row['f1_long'] = $aLng;
227
            if ($type1) {
228
                $row[$type1.'_lat']  = $aLat;
229
                $row[$type1.'_long'] = $aLng;
230
            }
231
        }
232
        unset($row['f1']);
233
    }
234
    if (array_key_exists('f2', $row)) {
235
        [$bLat, $bLng] = extractLatLng($row['f2']);
236
        if ($bLat !== null && $bLng !== null) {
237
            $row['f2_lat']  = $bLat;
238
            $row['f2_long'] = $bLng;
239
            if ($type2) {
240
                $row[$type2.'_lat']  = $bLat;
241
                $row[$type2.'_long'] = $bLng;
242
            }
243
        }
244
        unset($row['f2']);
245
    }
246
}
247
unset($row);
248
249
$data = array_values(array_filter($data, static function ($r) {
250
    return isset($r['f1_lat'],$r['f1_long'])
251
        || isset($r['f2_lat'],$r['f2_long'])
252
        || isset($r['ville_lat'],$r['ville_long'])
253
        || isset($r['stage_lat'],$r['stage_long']);
254
}));
255
256
/* ---------------- render ------------------------------- */
257
$htmlHeadXtra[] = '<script type="text/javascript" src="'.api_get_path(WEB_LIBRARY_JS_PATH).'map/markerclusterer.js"></script>';
258
$htmlHeadXtra[] = '<script type="text/javascript" src="'.api_get_path(WEB_LIBRARY_JS_PATH).'map/oms.min.js"></script>';
259
260
$tpl = new Template(null);
261
$tpl->assign('url', api_get_path(WEB_PATH).'social');
262
$tpl->assign('places', json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
263
$tpl->assign('api_key', $apiKey);
264
$tpl->assign('gmap_api_key', $apiKey);
265
$tpl->assign('field_1', !empty($info1) ? ($info1['display_text'] ?? $var1 ?? '') : '');
266
$tpl->assign('field_2', !empty($info2) ? ($info2['display_text'] ?? $var2 ?? '') : '');
267
268
/* Icons (if your template uses them) */
269
$tpl->assign('image_city', Display::return_icon('red-dot.png', '', [], ICON_SIZE_SMALL, false, true));
270
$tpl->assign('image_stage', Display::return_icon('blue-dot.png', '', [], ICON_SIZE_SMALL, false, true));
271
272
$layout = $tpl->get_template('social/map.tpl');
273
$tpl->display($layout);
274