Passed
Pull Request — master (#6633)
by
unknown
10:24
created

resolveGeoType()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 4
c 1
b 0
f 1
nc 4
nop 0
dl 0
loc 5
rs 10
1
<?php
2
/* For licensing terms, see /license.txt */
3
4
/**
5
 * Social map with user geolocation (PluginHelper + Repository-based, APCu-safe)
6
 * - Validates plugin enabled for current access URL
7
 * - Reads config from access_url_rel_plugin.configuration
8
 * - Auto-creates missing extra fields (admin only)
9
 * - Supports JSON {"lat","lng"} and legacy "label::lat,lng" or "lat,lng"
10
 * - LEFT JOIN: shows markers if user filled at least one field
11
 * - Cache: APCu if available, otherwise FilesystemAdapter
12
 */
13
14
use Chamilo\CoreBundle\Framework\Container;
15
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
16
use Chamilo\CoreBundle\Helpers\PluginHelper;
17
use Chamilo\CoreBundle\Repository\AccessUrlRelPluginRepository;
18
use Symfony\Component\Cache\Adapter\ApcuAdapter;
19
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
20
21
$cidReset = true;
22
require_once __DIR__.'/../inc/global.inc.php';
23
24
api_block_anonymous_users();
25
26
/* --------------------------------------------------
27
 * Helpers
28
 * -------------------------------------------------- */
29
function extractLatLng($raw) {
30
    if (empty($raw)) return [null, null];
31
    $raw = trim($raw);
32
33
    // JSON {"lat":...,"lng":...}
34
    if (strlen($raw) > 1 && $raw[0] === '{') {
35
        $obj = json_decode($raw, true);
36
        if (json_last_error() === JSON_ERROR_NONE && isset($obj['lat'], $obj['lng'])) {
37
            return [$obj['lat'], $obj['lng']];
38
        }
39
    }
40
    // Legacy "label::lat,lng"
41
    if (strpos($raw, '::') !== false) {
42
        [, $coords] = explode('::', $raw, 2);
43
        $p = array_map('trim', explode(',', $coords, 2));
44
        if (count($p) === 2) return [$p[0], $p[1]];
45
    }
46
    // Simple "lat,lng"
47
    if (strpos($raw, ',') !== false) {
48
        $p = array_map('trim', explode(',', $raw, 2));
49
        if (count($p) === 2) return [$p[0], $p[1]];
50
    }
51
    return [null, null];
52
}
53
54
function resolveGeoType(): int {
55
    if (defined('ExtraField::FIELD_TYPE_GEOLOCALIZATION')) return constant('ExtraField::FIELD_TYPE_GEOLOCALIZATION');
56
    if (defined('ExtraField::FIELD_TYPE_GEOLOCATION'))   return constant('ExtraField::FIELD_TYPE_GEOLOCATION');
57
    if (defined('ExtraField::FIELD_TYPE_TEXT'))          return constant('ExtraField::FIELD_TYPE_TEXT');
58
    return 1; // fallback (TEXT)
59
}
60
61
function ensureGeoExtraField(ExtraField $ef, ?string $var, string $label): ?array {
62
    if (empty($var)) return null;
63
    $info = $ef->get_handler_field_info_by_field_variable($var);
64
    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...
65
66
    // Create the field only for platform admins
67
    if (api_is_platform_admin()) {
68
        $payload = [
69
            'variable'           => $var,
70
            'display_text'       => $label,
71
            'value_type'         => resolveGeoType(),
72
            'visible_to_self'    => 1,
73
            'visible_to_others'  => 1,
74
            'changeable'         => 1,
75
            'created_at'         => date('Y-m-d H:i:s'),
76
        ];
77
        $ef->save($payload);
78
        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...
79
    }
80
    return null;
81
}
82
83
/* --------------------------------------------------
84
 * 1) Load helpers (PluginHelper, AccessUrlHelper, Repository)
85
 * -------------------------------------------------- */
86
$pluginHelper    = Container::$container
87
    ->get(PluginHelper::class);
88
$accessUrlHelper = Container::$container
89
    ->get(AccessUrlHelper::class);
90
$pluginRepo      = Container::$container
91
    ->get(AccessUrlRelPluginRepository::class);
92
93
$PLUGIN_NAME = 'google_maps';
94
95
/* Check plugin is enabled for the current access URL */
96
if (!$pluginHelper->isPluginEnabled($PLUGIN_NAME)) {
97
    if (api_is_platform_admin()) {
98
        Display::display_header(get_lang('Social'));
99
        echo Display::return_message(
100
            'Google Maps plugin is not enabled for this portal URL. Go to Administration → Plugins and enable it.',
101
            'warning'
102
        );
103
        echo '<p><a href="'.api_get_path(WEB_CODE_PATH).'admin/plugins.php">Open Plugins admin</a></p>';
104
        Display::display_footer();
105
        exit;
106
    }
107
    error_log('[social/map] DENY: plugin_disabled_in_url');
108
    api_not_allowed(true);
109
}
110
111
/* --------------------------------------------------
112
 * 2) Load plugin configuration from DB (by current URL)
113
 * -------------------------------------------------- */
114
$currentUrl = $accessUrlHelper->getCurrent();
115
if ($currentUrl === null) {
116
    error_log('[social/map] DENY: no current access URL');
117
    api_not_allowed(true);
118
}
119
120
$rel = $pluginRepo->findOneByPluginName($PLUGIN_NAME, $currentUrl->getId());
121
if (!$rel || !$rel->isActive()) {
122
    error_log('[social/map] DENY: plugin relation not active for url_id='.$currentUrl->getId());
123
    api_not_allowed(true);
124
}
125
126
$config = $rel->getConfiguration();
127
if (is_string($config)) {
128
    $decoded = json_decode($config, true);
129
    if (json_last_error() === JSON_ERROR_NONE) {
130
        $config = $decoded;
131
    }
132
}
133
if (!is_array($config)) $config = [];
134
135
$enabledRaw = $config['enable_api'] ?? null;
136
$apiKey     = (string) ($config['api_key'] ?? '');
137
138
/* Accept common truthy string/number values */
139
$localization = ($enabledRaw === true)
140
    || ($enabledRaw === 1)
141
    || ($enabledRaw === '1')
142
    || ($enabledRaw === 'true')
143
    || ($enabledRaw === 'on')
144
    || ($enabledRaw === 'yes');
145
146
if (!$localization || $apiKey === '') {
147
    if (api_is_platform_admin()) {
148
        Display::display_header(get_lang('Social'));
149
        echo Display::return_message(
150
            'Google Maps plugin is not configured. Enable the API and set your API key in Administration → Plugins → Google Maps.',
151
            'warning'
152
        );
153
        echo '<p><a href="'.api_get_path(WEB_CODE_PATH).'admin/plugins.php">Open Plugins admin</a></p>';
154
        Display::display_footer();
155
        exit;
156
    }
157
    error_log('[social/map] DENY: plugin_not_configured (enable_api='.var_export($enabledRaw,true).', api_key_present='.(int)($apiKey!=='').')');
158
    api_not_allowed(true);
159
}
160
161
/* --------------------------------------------------
162
 * 3) Fields to use (from plugin.extra_field_name or legacy setting)
163
 * -------------------------------------------------- */
164
$pluginFieldsCsv = (string) ($config['extra_field_name'] ?? '');
165
$vars = array_values(array_filter(array_map('trim', explode(',', $pluginFieldsCsv))));
166
167
if (empty($vars)) {
168
    $fieldsSetting = api_get_setting('profile.allow_social_map_fields', true);
169
    if (!$fieldsSetting || empty($fieldsSetting['fields']) || !is_array($fieldsSetting['fields'])) {
170
        error_log('[social/map] DENY: no fields configured (plugin.extra_field_name empty, allow_social_map_fields empty)');
171
        api_not_allowed(true);
172
    }
173
    $vars = array_values($fieldsSetting['fields']);
174
}
175
176
/* Keep at most 2 distinct variables */
177
$vars = array_values(array_unique(array_filter($vars)));
178
$var1 = $vars[0] ?? null; // e.g. terms_villedustage
179
$var2 = $vars[1] ?? null; // e.g. terms_ville
180
181
/* --------------------------------------------------
182
 * 4) Ensure extra fields exist (admin can auto-create), then continue if at least one is present
183
 * -------------------------------------------------- */
184
$extraField = new ExtraField('user');
185
$info1 = ensureGeoExtraField($extraField, $var1, $var1 ?: 'Geolocation A');
186
$info2 = ensureGeoExtraField($extraField, $var2, $var2 ?: 'Geolocation B');
187
188
if (empty($info1) && empty($info2)) {
189
    error_log('[social/map] DENY: missing both extrafields and cannot create (vars='.json_encode($vars).')');
190
    api_not_allowed(true);
191
}
192
193
/* --------------------------------------------------
194
 * 5) Query users (LEFT JOIN per existing field) - allow users with at least one value
195
 * -------------------------------------------------- */
196
$tableUser = Database::get_main_table(TABLE_MAIN_USER);
197
198
$select = "u.id, u.firstname, u.lastname";
199
$joins  = [];
200
$conds  = [];
201
202
if (!empty($info1)) {
203
    $select .= ", ev1.field_value AS f1";
204
    $joins[] = "LEFT JOIN extra_field_values ev1
205
                  ON ev1.item_id = u.id
206
                 AND ev1.field_id = ".$info1['id'];
207
    $conds[] = "COALESCE(ev1.field_value,'') <> ''";
208
}
209
if (!empty($info2)) {
210
    $select .= ", ev2.field_value AS f2";
211
    $joins[] = "LEFT JOIN extra_field_values ev2
212
                  ON ev2.item_id = u.id
213
                 AND ev2.field_id = ".$info2['id'];
214
    $conds[] = "COALESCE(ev2.field_value,'') <> ''";
215
}
216
217
if (empty($conds)) {
218
    error_log('[social/map] DENY: no join conditions built');
219
    api_not_allowed(true);
220
}
221
222
$sql = "SELECT $select
223
        FROM $tableUser u
224
        ".implode("\n", $joins)."
225
        WHERE u.active = 1
226
          AND u.status = ".STUDENT."
227
          AND (".implode(' OR ', $conds).")";
228
229
/* --------------------------------------------------
230
 * 6) Cache with fallback (APCu → Filesystem)
231
 * -------------------------------------------------- */
232
$useApcu = false;
233
if (function_exists('apcu_enabled')) {
234
    $useApcu = apcu_enabled();
235
} else {
236
    $useApcu = extension_loaded('apcu') && (PHP_SAPI !== 'cli' || (bool)ini_get('apc.enable_cli'));
237
}
238
239
if ($useApcu) {
240
    $cache = new ApcuAdapter('social_map');
241
} else {
242
    // Filesystem fallback
243
    $cacheDir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.'chamilo_cache';
244
    if (!is_dir($cacheDir)) {
245
        @mkdir($cacheDir, 0775, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

245
        /** @scrutinizer ignore-unhandled */ @mkdir($cacheDir, 0775, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
246
    }
247
    $cache = new FilesystemAdapter('social_map', 300, $cacheDir);
248
}
249
250
$keyDate = 'map_cache_date';
251
$keyData = 'map_cache_data';
252
253
$now     = time();
254
$expires = strtotime('+5 minute', $now);
255
256
/* Force refresh (keep same behavior as original code) */
257
$loadFromDatabase = true;
258
259
if ($loadFromDatabase) {
260
    $result = Database::query($sql);
261
    $data   = Database::store_result($result, 'ASSOC');
262
263
    $cacheItem = $cache->getItem($keyData);
264
    $cacheItem->set($data);
265
    $cache->save($cacheItem);
266
267
    $cacheItem = $cache->getItem($keyDate);
268
    $cacheItem->set($expires);
269
    $cache->save($cacheItem);
270
} else {
271
    $data = $cache->getItem($keyData)->get();
272
}
273
274
/* --------------------------------------------------
275
 * 7) Parse coordinates and prepare payload for the template
276
 * -------------------------------------------------- */
277
foreach ($data as &$row) {
278
    $row['complete_name'] = $row['firstname'].' '.$row['lastname'];
279
    $row['lastname']  = '';
280
    $row['firstname'] = '';
281
282
    if (array_key_exists('f1', $row)) {
283
        [$aLat, $aLng] = extractLatLng($row['f1']);
284
        if ($aLat !== null && $aLng !== null) {
285
            $row['f1_lat']  = $aLat;
286
            $row['f1_long'] = $aLng;
287
        }
288
        unset($row['f1']);
289
    }
290
    if (array_key_exists('f2', $row)) {
291
        [$bLat, $bLng] = extractLatLng($row['f2']);
292
        if ($bLat !== null && $bLng !== null) {
293
            $row['f2_lat']  = $bLat;
294
            $row['f2_long'] = $bLng;
295
        }
296
        unset($row['f2']);
297
    }
298
}
299
300
/* --------------------------------------------------
301
 * 8) Assets + render
302
 * -------------------------------------------------- */
303
$htmlHeadXtra[] = '<script type="text/javascript" src="'.api_get_path(WEB_LIBRARY_JS_PATH).'map/markerclusterer.js"></script>';
304
$htmlHeadXtra[] = '<script type="text/javascript" src="'.api_get_path(WEB_LIBRARY_JS_PATH).'map/oms.min.js"></script>';
305
306
$tpl = new Template(null);
307
$tpl->assign('url', api_get_path(WEB_PATH).'social');
308
$tpl->assign('places', json_encode($data));
309
$tpl->assign('api_key', $apiKey);       // typical variable name used by templates
310
$tpl->assign('gmap_api_key', $apiKey);  // also assign the legacy name, just in case
311
312
/* Labels (avoid notices if only one field exists) */
313
$tpl->assign('field_1', !empty($info1) ? ($info1['display_text'] ?? $var1 ?? '') : '');
314
$tpl->assign('field_2', !empty($info2) ? ($info2['display_text'] ?? $var2 ?? '') : '');
315
316
/* Icons (if your template uses them) */
317
$tpl->assign(
318
    'image_city',
319
    Display::return_icon('red-dot.png', '', [], ICON_SIZE_SMALL, false, true)
320
);
321
$tpl->assign(
322
    'image_stage',
323
    Display::return_icon('blue-dot.png', '', [], ICON_SIZE_SMALL, false, true)
324
);
325
326
$layout = $tpl->get_template('social/map.tpl');
327
$tpl->display($layout);
328