1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8class Services_Tracker_SyncController 9{ 10 private $utilities; 11 12 function setUp() 13 { 14 global $prefs; 15 $this->utilities = new Services_Tracker_Utilities; 16 17 if ($prefs['feature_trackers'] != 'y') { 18 throw new Services_Exception_Disabled('feature_trackers'); 19 } 20 21 if ($prefs['tracker_remote_sync'] != 'y') { 22 throw new Services_Exception_Disabled('tracker_remote_sync'); 23 } 24 25 if (! Perms::get()->admin_trackers) { 26 throw new Services_Exception(tr('Reserved for tracker administrators'), 403); 27 } 28 } 29 30 function action_clone_remote($input) 31 { 32 $url = $input->url->url(); 33 $remoteTracker = $input->remote_tracker_id->int(); 34 35 if ($url) { 36 $url = rtrim($url, '/'); 37 $tracker = $this->findTrackerInfo($url, $remoteTracker); 38 39 if (! $tracker) { 40 // Prepare the list for tracker selection 41 $trackers = $this->getRemoteTrackerList($url); 42 return [ 43 'url' => $url, 44 'list' => $trackers['list'], 45 ]; 46 } else { 47 // Proceed with the tracker import 48 $export = $this->getRemoteTrackerFieldExport($url, $remoteTracker); 49 50 $trackerId = $this->utilities->createTracker($tracker); 51 $this->createSynchronizedFields( 52 $trackerId, 53 $export, 54 ['provider' => $url, 'source' => $remoteTracker, 'last' => 0] 55 ); 56 $this->utilities->createField( 57 [ 58 'trackerId' => $trackerId, 59 'type' => 't', 60 'name' => tr('Remote Source'), 61 'permName' => 'syncSource', 62 'description' => tr('Automatically generated field for synchronized trackers. Contains the itemId of the remote item.'), 63 'options' => $this->utilities->buildOptions( 64 ['prepend' => $url . '/item'], 65 't' 66 ), 67 'isMandatory' => false, 68 ] 69 ); 70 71 $this->registerSynchronization($trackerId, $url, $remoteTracker); 72 73 return [ 74 'trackerId' => $trackerId, 75 ]; 76 } 77 } 78 79 return [ 80 'url' => $url, 81 'title' => tr('Clone Remote Tracker'), 82 ]; 83 } 84 85 function action_sync_meta($input) 86 { 87 list($trackerId, $definition, $syncInfo) = $this->readTracker($input); 88 $factory = $definition->getFieldFactory(); 89 90 $export = $this->getRemoteTrackerFieldExport($syncInfo['provider'], $syncInfo['source']); 91 foreach ($export as $info) { 92 $localField = $definition->getFieldFromPermName($info['permName']); 93 if (! $localField) { 94 continue; 95 } 96 97 $handler = $factory->getHandler($info); 98 if (! $handler instanceof Tracker_Field_Synchronizable) { 99 continue; 100 } 101 102 $importable = $handler->importRemoteField($info, $syncInfo); 103 $this->utilities->updateField($trackerId, $localField['fieldId'], $importable); 104 } 105 106 return []; 107 } 108 109 function action_sync_refresh($input) 110 { 111 list($trackerId, $definition, $syncInfo) = $this->readTracker($input); 112 113 set_time_limit(0); // Expected to take a while on larger trackers 114 115 $this->utilities->clearTracker($trackerId); 116 117 $itemMap = []; 118 119 $remoteDefinition = $this->getRemoteDefinition($definition); 120 $factory = $remoteDefinition->getFieldFactory(); 121 foreach ($this->getRemoteItems($syncInfo) as $item) { 122 foreach ($item['fields'] as $key => & $value) { 123 $field = $remoteDefinition->getFieldFromPermName($key); 124 if ($field && $definition->getFieldFromPermName($key)) { 125 $handler = $factory->getHandler($field); 126 $value = $handler->importRemote($value); 127 } 128 } 129 130 $item['fields']['syncSource'] = $item['itemId']; 131 $newItem = $this->utilities->insertItem($definition, $item); 132 133 $itemMap[ $item['itemId'] ] = $newItem; 134 } 135 136 if ($definition->getLanguageField()) { 137 $this->attachTranslations($syncInfo, 'trackeritem', $itemMap); 138 } 139 140 $this->registerSynchronization($trackerId, $syncInfo['provider'], $syncInfo['source']); 141 TikiLib::lib('unifiedsearch')->processUpdateQueue(count($itemMap) * 3); // Process lots of inserts 142 return []; 143 } 144 145 function action_sync_new($input) 146 { 147 list($trackerId, $definition, $syncInfo) = $this->readTracker($input); 148 149 $items = $input->items->int(); 150 151 $trklib = TikiLib::lib('trk'); 152 $syncField = $definition->getFieldFromPermName('syncSource'); 153 $itemIds = $trklib->get_items_list($trackerId, $syncField['fieldId'], '', 'opc'); 154 155 if ($items) { 156 set_time_limit(30 + 10 * count($items)); // 10 sec per item plus some initial overhead 157 $itemIds = array_intersect($itemIds, $items); 158 $table = TikiDb::get()->table('tiki_tracker_items'); 159 $items = $this->utilities->getItems(['trackerId' => $trackerId, 'itemId' => $table->in($itemIds)]); 160 161 $remoteDefinition = $this->getRemoteDefinition($definition); 162 foreach ($items as $item) { 163 $remoteItemId = $this->insertRemoteItem($remoteDefinition, $definition, $item); 164 165 if ($remoteItemId) { 166 $item['fields']['syncSource'] = $remoteItemId; 167 $this->utilities->updateItem($definition, $item); 168 } 169 } 170 TikiLib::lib('unifiedsearch')->processUpdateQueue(); 171 172 return [ 173 ]; 174 } else { 175 return [ 176 'trackerId' => $trackerId, 177 'sets' => ['items'], 178 'items' => $this->getItemList($itemIds), 179 ]; 180 } 181 } 182 183 function action_sync_edit($input) 184 { 185 list($trackerId, $definition, $syncInfo) = $this->readTracker($input); 186 187 // Collect local IDs that were modified 188 $items = TikiDb::get()->table('tiki_tracker_items'); 189 $itemIds = $items->fetchColumn( 190 'itemId', 191 [ 192 'trackerId' => $trackerId, 193 'created' => $items->lesserThan($syncInfo['last']), 194 'lastModif' => $items->greaterThan($syncInfo['last']), 195 ] 196 ); 197 198 // Collect remote IDs that were modified 199 $remoteItems = $this->getRemoteItems($syncInfo, ['modifiedSince' => $syncInfo['last']]); 200 201 $modifiedIds = []; 202 foreach ($remoteItems as $item) { 203 $modifiedIds[] = $item['itemId']; 204 } 205 206 // Map from remote ID to local ID 207 $syncField = $definition->getFieldFromPermName('syncSource'); 208 $fields = TikiDb::get()->table('tiki_tracker_item_fields'); 209 $itemMap = $fields->fetchMap( 210 'itemId', 211 'value', 212 ['fieldId' => $syncField['fieldId'], 'value' => $fields->in($modifiedIds)] 213 ); 214 215 $modifiedIds = array_keys($itemMap); 216 $automatic = array_diff($itemIds, $modifiedIds); 217 $manual = array_intersect($itemIds, $modifiedIds); 218 219 set_time_limit(30 + 10 * count($automatic) + 10 * count($manual)); // 10 sec per item plus some initial overhead 220 221 if ($input->automatic->int() || $input->manual->int()) { 222 $remoteDefinition = $this->getRemoteDefinition($definition); 223 $this->processUpdates('automatic', $automatic, $input, $definition, $remoteDefinition); 224 $this->processUpdates('manual', $manual, $input, $definition, $remoteDefinition); 225 } 226 227 $manualList = $this->getItemList($manual); 228 require_once 'lib/smarty_tiki/modifier.sefurl.php'; 229 foreach ($manualList as & $item) { 230 $itemId = $item['itemId']; 231 $item['remoteUrl'] = $syncInfo['provider'] . '/' . smarty_modifier_sefurl($itemMap[$itemId], 'trackeritem'); 232 } 233 234 return [ 235 'trackerId' => $trackerId, 236 'sets' => ['automatic', 'manual'], 237 'automatic' => $this->getItemList($automatic), 238 'manual' => $manualList, 239 ]; 240 } 241 242 private function createSynchronizedFields($trackerId, $data, $syncInfo) 243 { 244 if (! $data) { 245 throw new Services_Exception(tr('Invalid data provided'), 400); 246 } 247 248 $definition = Tracker_Definition::get($trackerId); 249 $factory = $definition->getFieldFactory(); 250 foreach ($data as $info) { 251 $handler = $factory->getHandler($info); 252 if ($handler instanceof Tracker_Field_Synchronizable) { 253 $importable = $handler->importRemoteField($info, $syncInfo); 254 $this->utilities->importField($trackerId, new JitFilter($importable), false); 255 } 256 } 257 } 258 259 private function getRemoteTrackerList($serviceUrl) 260 { 261 static $cache = []; 262 if (isset($cache[$serviceUrl])) { 263 return $cache[$serviceUrl]; 264 } 265 266 $controller = new Services_RemoteController($serviceUrl, 'tracker'); 267 $data = $controller->list_trackers(); 268 return $cache[$serviceUrl] = $data; 269 } 270 271 private function getRemoteTrackerFieldExport($serviceUrl, $trackerId) 272 { 273 $controller = new Services_RemoteController($serviceUrl, 'tracker'); 274 $export = $controller->export_fields(['trackerId' => $trackerId]); 275 276 return TikiLib::lib('tiki')->read_raw($export['export']); 277 } 278 279 private function findTrackerInfo($serviceUrl, $trackerId) 280 { 281 $trackers = $this->getRemoteTrackerList($serviceUrl); 282 foreach ($trackers['data'] as $info) { 283 if ($info['trackerId'] == $trackerId) { 284 unset($info['trackerId']); 285 return $info; 286 } 287 } 288 } 289 290 private function registerSynchronization($localTrackerId, $serviceUrl, $remoteTrackerId) 291 { 292 $attributelib = TikiLib::lib('attribute'); 293 $attributelib->set_attribute('tracker', $localTrackerId, 'tiki.sync.provider', rtrim($serviceUrl, '/')); 294 $attributelib->set_attribute('tracker', $localTrackerId, 'tiki.sync.source', $remoteTrackerId); 295 $attributelib->set_attribute('tracker', $localTrackerId, 'tiki.sync.last', time()); // Real sync time, not tiki initial load 296 } 297 298 private function getRemoteItems($syncInfo, array $conditions = []) 299 { 300 $controller = new Services_RemoteController($syncInfo['provider'], 'tracker'); 301 return $controller->getResultLoader( 302 'list_items', 303 array_merge($conditions, ['trackerId' => $syncInfo['source'], 'format' => 'raw']), 304 'offset', 305 'maxRecords', 306 'result' 307 ); 308 } 309 310 private function insertRemoteItem($remoteDefinition, $definition, $item) 311 { 312 $syncInfo = $definition->getSyncInformation(); 313 314 $item['trackerId'] = $syncInfo['source']; 315 $item['fields'] = $this->exportFields($item['fields'], $remoteDefinition, $definition); 316 317 $controller = new Services_RemoteController($syncInfo['provider'], 'tracker'); 318 $data = $controller->insert_item($item); 319 320 if (isset($data['itemId']) && $data['itemId']) { 321 return $data['itemId']; 322 } 323 } 324 325 private function updateRemoteItem($remoteDefinition, $definition, $item) 326 { 327 $syncInfo = $definition->getSyncInformation(); 328 329 $item['itemId'] = $item['fields']['syncSource']; 330 $item['trackerId'] = $syncInfo['source']; 331 332 $item['fields'] = $this->exportFields($item['fields'], $remoteDefinition, $definition); 333 334 $controller = new Services_RemoteController($syncInfo['provider'], 'tracker'); 335 $controller->update_item($item); 336 } 337 338 private function exportFields($fields, $remoteDefinition, $definition) 339 { 340 unset($fields['syncSource']); 341 $factory = $definition->getFieldFactory(); 342 foreach ($fields as $key => & $value) { 343 $field = $remoteDefinition->getFieldFromPermName($key); 344 if ($field && $definition->getFieldFromPermName($key)) { 345 $handler = $factory->getHandler($field); 346 $value = $handler->exportRemote($value); 347 } 348 } 349 350 return $fields; 351 } 352 353 private function attachTranslations($syncInfo, $type, $objectMap) 354 { 355 $unprocessed = $objectMap; 356 $utilities = new Services_Language_Utilities; 357 358 while (reset($unprocessed)) { 359 $remoteSource = key($unprocessed); 360 361 unset($unprocessed[$remoteSource]); 362 363 $translations = $this->getRemoteTranslations($syncInfo, $type, $remoteSource); 364 foreach ($translations as $remoteTarget) { 365 unset($unprocessed[$remoteTarget]); 366 $utilities->insertTranslation($type, $objectMap[ $remoteSource ], $objectMap[ $remoteTarget ]); 367 } 368 } 369 } 370 371 private function getRemoteTranslations($syncInfo, $type, $remoteSource) 372 { 373 $controller = new Services_RemoteController($syncInfo['provider'], 'translation'); 374 $data = $controller->manage(['type' => $type, 'source' => $remoteSource]); 375 376 $out = []; 377 378 if ($data['translations']) { 379 foreach ($data['translations'] as $translation) { 380 if ($translation['objId'] != $remoteSource) { 381 $out[] = $translation['objId']; 382 } 383 } 384 } 385 386 return $out; 387 } 388 389 private function getRemoteDefinition($definition) 390 { 391 $syncInfo = $definition->getSyncInformation(); 392 393 return Tracker_Definition::createFake( 394 $definition->getInformation(), 395 $this->getRemoteTrackerFieldExport($syncInfo['provider'], $syncInfo['source']) 396 ); 397 } 398 399 private function readTracker($input) 400 { 401 $trackerId = $input->trackerId->int(); 402 $definition = Tracker_Definition::get($trackerId); 403 404 if (! $definition) { 405 throw new Services_Exception(tr('Tracker does not exist'), 404); 406 } 407 408 $syncInfo = $definition->getSyncInformation(); 409 410 if (! $syncInfo) { 411 throw new Services_Exception(tr('Tracker is not synchronized with a remote source.'), 409); 412 } 413 414 return [$trackerId, $definition, $syncInfo]; 415 } 416 417 private function getItemList($itemIds) 418 { 419 $trklib = TikiLib::lib('trk'); 420 require_once 'lib/smarty_tiki/modifier.sefurl.php'; 421 422 $out = []; 423 foreach ($itemIds as $itemId) { 424 $out[] = [ 425 'itemId' => $itemId, 426 'title' => $trklib->get_isMain_value(null, $itemId), 427 'localUrl' => smarty_modifier_sefurl($itemId, 'trackeritem'), 428 ]; 429 } 430 431 return $out; 432 } 433 434 private function processUpdates($inputType, & $list, $input, $definition, $remoteDefinition) 435 { 436 $values = $input->$inputType->int(); 437 if (! is_array($values)) { 438 return; 439 } 440 441 $toProcess = array_intersect($list, $values); 442 $list = array_diff($list, $values); 443 444 $table = TikiDb::get()->table('tiki_tracker_items'); 445 $itemList = $this->utilities->getItems(['trackerId' => $definition->getConfiguration('trackerId'), 'itemId' => $table->in($toProcess)]); 446 foreach ($itemList as $item) { 447 $this->updateRemoteItem($remoteDefinition, $definition, $item); 448 $this->utilities->removeItem($item['itemId']); 449 } 450 } 451} 452