chamilo /
chamilo-lms
| 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
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
|
|||
| 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 |