l10n_update.check.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. <?php
  2. /**
  3. * @file
  4. * Reusable API for l10n remote updates using $source objects
  5. *
  6. * These functions may not be safe for the installer as they use variables and report using watchdog
  7. */
  8. /**
  9. * Threshold for timestamp comparison.
  10. *
  11. * Eliminates a difference between the download time
  12. * (Database: l10n_update_file.timestamp) and the actual .po file timestamp.
  13. */
  14. define('L10N_UPDATE_TIMESTAMP_THRESHOLD', 2);
  15. module_load_include('inc', 'l10n_update');
  16. /**
  17. * Fetch update information for all projects / all languages.
  18. *
  19. * @param boolean $refresh
  20. * TRUE = refresh the release data.
  21. * We refresh anyway if the data is older than a day.
  22. *
  23. * @return array
  24. * Available releases indexed by project and language.
  25. */
  26. function l10n_update_available_releases($refresh = FALSE) {
  27. $frequency = variable_get('l10n_update_check_frequency', 0) * 24 * 3600;
  28. if (!$refresh && ($cache = cache_get('l10n_update_available_releases', 'cache_l10n_update')) && (!$frequency || $cache->created > REQUEST_TIME - $frequency)) {
  29. return $cache->data;
  30. }
  31. else {
  32. $projects = l10n_update_get_projects(TRUE);
  33. $languages = l10n_update_language_list();
  34. $available = l10n_update_check_projects($projects, array_keys($languages));
  35. cache_set('l10n_update_available_releases', $available, 'cache_l10n_update', $frequency ? REQUEST_TIME + $frequency : CACHE_PERMANENT);
  36. return $available;
  37. }
  38. }
  39. /**
  40. * Check latest release for project, language.
  41. *
  42. * @param $projects
  43. * Projects to check (objects).
  44. * @param $languages
  45. * Array of language codes to check, none to check all.
  46. * @param $check_local
  47. * Check local translation file.
  48. * @param $check_remote
  49. * Check remote translation file.
  50. *
  51. * @return array
  52. * Available sources indexed by project, language.
  53. */
  54. function l10n_update_check_projects($projects, $languages = NULL, $check_local = NULL, $check_remote = NULL) {
  55. if (!isset($check_local)) {
  56. $check_local = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL);
  57. }
  58. if (!isset($check_remote)) {
  59. $check_remote = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE);
  60. }
  61. $languages = $languages ? $languages : array_keys(l10n_update_language_list());
  62. $result = array();
  63. foreach ($projects as $name => $project) {
  64. foreach ($languages as $lang) {
  65. $source = l10n_update_source_build($project, $lang);
  66. if ($update = l10n_update_source_check($source, $check_local, $check_remote)) {
  67. $result[$name][$lang] = $update;
  68. }
  69. }
  70. }
  71. return $result;
  72. }
  73. /**
  74. * Compare available releases with history and get list of downloadable updates.
  75. *
  76. * @param $history
  77. * Update history of projects.
  78. * @param $available
  79. * Available project releases.
  80. * @return array
  81. * Projects to be updated: 'not yet downloaded', 'newer timestamp available',
  82. * 'new version available'.
  83. * Up to date projects are not included in the array.
  84. */
  85. function l10n_update_build_updates($history, $available) {
  86. $updates = array();
  87. foreach ($available as $name => $project_updates) {
  88. foreach ($project_updates as $lang => $update) {
  89. if (!empty($update->timestamp)) {
  90. $current = !empty($history[$name][$lang]) ? $history[$name][$lang] : NULL;
  91. // Add when not current, timestamp newer or version difers (newer version)
  92. if (_l10n_update_source_compare($current, $update) == -1 || $current->version != $update->version) {
  93. $updates[$name][$lang] = $update;
  94. }
  95. }
  96. }
  97. }
  98. return $updates;
  99. }
  100. /**
  101. * Check updates for active projects and languages.
  102. *
  103. * @param $count
  104. * Number of package translations to check.
  105. * @param $before
  106. * Unix timestamp, check only updates that haven't been checked for this time.
  107. * @param $limit
  108. * Maximum number of updates to do. We check $count translations
  109. * but we stop after we do $limit updates.
  110. * @return array
  111. */
  112. function l10n_update_check_translations($count, $before, $limit = 1) {
  113. $projects = l10n_update_get_projects();
  114. $updated = $checked = array();
  115. // Select active projects x languages ordered by last checked time
  116. $q = db_select('l10n_update_project', 'p');
  117. $q->leftJoin('l10n_update_file', 'f', 'p.name = f.project');
  118. $q->innerJoin('languages', 'l', 'l.language = f.language');
  119. $q->condition('p.status', 1);
  120. $q->condition('l.enabled', 1);
  121. // If the file is not there, or it is there, but we did not check since $before.
  122. $q->condition(db_or()->isNull('f.status')->condition(db_and()->condition('f.status', 1)->condition('f.last_checked', $before, '<')));
  123. $q->range(0, $count);
  124. $q->fields('p', array('name'));
  125. $q->fields('f');
  126. $q->addField('l', 'language', 'lang');
  127. $q->orderBy('last_checked');
  128. $result = $q->execute();
  129. if ($result) {
  130. $local = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_LOCAL);
  131. $remote = (bool) (variable_get('l10n_update_check_mode', L10N_UPDATE_CHECK_ALL) & L10N_UPDATE_CHECK_REMOTE);
  132. foreach ($result as $check) {
  133. if (count($updated) >= $limit) {
  134. break;
  135. }
  136. $checked[] = $check;
  137. if (!empty($projects[$check->name])) {
  138. $project = $projects[$check->name];
  139. $update = NULL;
  140. $source = l10n_update_source_build($project, $check->lang);
  141. $current = $check->filename ? $check : NULL;
  142. if ($available = l10n_update_source_check($source, $local, $remote)) {
  143. if (!$current || _l10n_update_source_compare($current, $available) == -1 || $current->version != $available->version) {
  144. $update = $available;
  145. }
  146. }
  147. if ($update) {
  148. // The update functions will update data and timestamps too
  149. l10n_update_source_update($update, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
  150. $updated[] = $update;
  151. }
  152. elseif ($current) {
  153. // No update available, just update timestamp for this row
  154. db_update('l10n_update_file')
  155. ->fields(array(
  156. 'last_checked' => REQUEST_TIME,
  157. ))
  158. ->condition('project', $current->project)
  159. ->condition('language', $current->language)
  160. ->execute();
  161. }
  162. elseif ($source) {
  163. // Create a new record just for keeping last checked time
  164. $source->last_checked = REQUEST_TIME;
  165. drupal_write_record('l10n_update_file', $source);
  166. }
  167. }
  168. }
  169. }
  170. return array($checked, $updated);
  171. }
  172. /**
  173. * Build abstract translation source, to be mapped to a file or a download.
  174. *
  175. * @param $project
  176. * Project object.
  177. * @param $langcode
  178. * Language code.
  179. * @param $filename
  180. * File name of translation file. May contains placeholders.
  181. * @return object
  182. * Source object, which may have these properties:
  183. * - 'project': Project name.
  184. * - 'language': Language code.
  185. * - 'type': Source type 'download' or 'localfile'.
  186. * - 'uri': Local file path.
  187. * - 'fileurl': Remote file URL for downloads.
  188. * - 'filename': File name.
  189. * - 'keep': TRUE to keep the downloaded file.
  190. * - 'timestamp': Last update time of the file.
  191. */
  192. function l10n_update_source_build($project, $langcode, $filename = L10N_UPDATE_DEFAULT_FILENAME) {
  193. $source = clone $project;
  194. $source->project = $project->name;
  195. $source->language = $langcode;
  196. $source->filename = l10n_update_build_string($source, $filename);
  197. return $source;
  198. }
  199. /**
  200. * Check local and remote sources for the file.
  201. *
  202. * @param $source
  203. * Translation source object.
  204. * @see l10n_update_source_build()
  205. * @param $check_local
  206. * File object of local translation file.
  207. * @param $check_remote
  208. * File object of remote translation file.
  209. * @return object
  210. * File object of most recent translation; local or remote.
  211. */
  212. function l10n_update_source_check($source, $check_local = TRUE, $check_remote = TRUE) {
  213. $local = $remote = NULL;
  214. if ($check_local) {
  215. $check = clone $source;
  216. if (l10n_update_source_check_file($check)) {
  217. $local = $check;
  218. }
  219. }
  220. if ($check_remote) {
  221. $check = clone $source;
  222. if (l10n_update_source_check_download($check)) {
  223. $remote = $check;
  224. }
  225. }
  226. // Get remote if newer than local only, they both can be empty
  227. return _l10n_update_source_compare($local, $remote) < 0 ? $remote : $local;
  228. }
  229. /**
  230. * Check remote file object.
  231. *
  232. * @param $source
  233. * Remote translation file object. The object will be update
  234. * with data of the remote file:
  235. * - 'type': Fixed value 'download'.
  236. * - 'fileurl': File name and path.
  237. * - 'timestamp': Last updated time.
  238. * @see l10n_update_source_build()
  239. * @return object
  240. * An object containing the HTTP request headers, response code, headers,
  241. * data, redirect status and updated timestamp.
  242. * NULL if failure.
  243. */
  244. function l10n_update_source_check_download($source) {
  245. $url = l10n_update_build_string($source, $source->l10n_path);
  246. $result = l10n_update_http_check($url);
  247. if ($result && !empty($result->updated)) {
  248. $source->type = 'download';
  249. // There may have been redirects so we store the resulting url
  250. $source->fileurl = isset($result->redirect_url) ? $result->redirect_url : $url;
  251. $source->timestamp = $result->updated;
  252. return $result;
  253. }
  254. }
  255. /**
  256. * Check whether we've got the file in the filesystem under 'translations'.
  257. *
  258. * It will search, similar to modules and themes:
  259. * - translations
  260. * - sites/all/translations
  261. * - sites/mysite/translations
  262. *
  263. * Using name as the key will return just the last one found.
  264. *
  265. * @param $source
  266. * Translation file object. The object will be updated with data of local file.
  267. * - 'type': Fixed value 'localfile'.
  268. * - 'uri': File name and path.
  269. * - 'timestamp': Last updated time.
  270. * @see l10n_update_source_build()
  271. * @param $directory
  272. * Files directory.
  273. * @return Object
  274. * File object (filename, basename, name)
  275. * NULL if failure.
  276. */
  277. function l10n_update_source_check_file($source, $directory = 'translations') {
  278. $filename = '/' . preg_quote($source->filename) . '$/';
  279. // Using the 'name' key will return
  280. if ($files = drupal_system_listing($filename, $directory, 'name', 0)) {
  281. $file = current($files);
  282. $source->type = 'localfile';
  283. $source->uri = $file->uri;
  284. $source->timestamp = filemtime($file->uri);
  285. return $file;
  286. }
  287. }
  288. /**
  289. * Download and import or just import source, depending on type.
  290. *
  291. * @param $source
  292. * Translation source object with information about the file location.
  293. * Object will be updated with :
  294. * - 'last_checked': Timestamp of current time;
  295. * - 'import_date': Timestamp of current time;
  296. * @param $mode
  297. * Download mode. How to treat exising and modified translations.
  298. * @return boolean
  299. * TRUE on success, NULL on failure.
  300. */
  301. function l10n_update_source_update($source, $mode) {
  302. if ($source->type == 'localfile' || l10n_update_source_download($source)) {
  303. if (l10n_update_source_import($source, $mode)) {
  304. l10n_update_source_history($source);
  305. return TRUE;
  306. }
  307. }
  308. }
  309. /**
  310. * Import source into locales table.
  311. *
  312. * @param $source
  313. * Translation source object with information about the file location.
  314. * Object will be updated with :
  315. * - 'last_checked': Timestamp of current time;
  316. * - 'import_date': Timestamp of current time;
  317. * @param $mode
  318. * Download mode. How to treat exising and modified translations.
  319. * @return boolean
  320. * Result array on success, FALSE on failure.
  321. */
  322. function l10n_update_source_import($source, $mode) {
  323. if (!empty($source->uri) && $result = l10n_update_import_file($source->uri, $source->language, $mode)) {
  324. $source->last_checked = REQUEST_TIME;
  325. // We override the file timestamp here. The default file time stamp is the
  326. // release date from the l.d.o server. We change the timestamp to the
  327. // creation time on the webserver. On multi sites that share a common
  328. // sites/all/translations directory, the sharing sites use the local file
  329. // creation date as release date. Without this correction the local
  330. // file is always newer than the l.d.o. file, which results in unnecessary
  331. // translation import.
  332. $source->timestamp = time();
  333. return $result;
  334. }
  335. }
  336. /**
  337. * Download source file from remote server.
  338. *
  339. * If succesful this function returns the downloaded file in two ways:
  340. * - As a temporary $file object
  341. * - As a file path on the $source->uri property.
  342. *
  343. * @param $source
  344. * Source object with all parameters
  345. * - 'fileurl': url to download.
  346. * - 'uri': alternate destination. If not present a temporary file
  347. * will be used and the path returned here.
  348. * @return object
  349. * $file object if download successful.
  350. */
  351. function l10n_update_source_download($source) {
  352. if (!empty($source->uri)) {
  353. $destination = $source->uri;
  354. }
  355. elseif ($directory = variable_get('l10n_update_download_store', '')) {
  356. $destination = $directory . '/' . $source->filename;
  357. }
  358. else {
  359. $destination = NULL;
  360. }
  361. if ($file = l10n_update_download_file($source->fileurl, $destination)) {
  362. $source->uri = $file;
  363. return $file;
  364. }
  365. }
  366. /**
  367. * Update the file history table and delete the file if temporary.
  368. *
  369. * @param $file
  370. * Source object representing the file just imported or downloaded.
  371. */
  372. function l10n_update_source_history($file) {
  373. // Update history table
  374. l10n_update_file_history($file);
  375. // If it's a downloaded file and not marked for keeping, delete the file.
  376. if ($file->type == 'download' && empty($file->keep)) {
  377. file_unmanaged_delete($file->uri);
  378. $file->uri = '';
  379. }
  380. }
  381. /**
  382. * Compare two update sources, looking for the newer one (bigger timestamp).
  383. *
  384. * This function can be used as a callback to compare two source objects.
  385. *
  386. * @param $current
  387. * Source object of current project.
  388. * @param $update
  389. * Source object of available update.
  390. * @return integer
  391. * - '-1': $current < $update OR $current is missing
  392. * - '0': $current == $update OR both $current and $updated are missing
  393. * - '1': $current > $update OR $update is missing
  394. */
  395. function _l10n_update_source_compare($current, $update) {
  396. if ($current && $update) {
  397. if (abs($current->timestamp - $update->timestamp) < L10N_UPDATE_TIMESTAMP_THRESHOLD) {
  398. return 0;
  399. }
  400. else {
  401. return $current->timestamp > $update->timestamp ? 1 : -1;
  402. }
  403. }
  404. elseif ($current && !$update) {
  405. return 1;
  406. }
  407. elseif (!$current && $update) {
  408. return -1;
  409. }
  410. else {
  411. return 0;
  412. }
  413. }
  414. /**
  415. * Prepare update list.
  416. *
  417. * @param $updates
  418. * Array of update sources that may be indexed in multiple ways.
  419. * @param $projects
  420. * Array of project names to be included, others will be filtered out.
  421. * @param $languages
  422. * Array of language codes to be included, others will be filtered out.
  423. * @return array
  424. * Plain array of filtered updates with directory applied.
  425. */
  426. function _l10n_update_prepare_updates($updates, $projects = NULL, $languages = NULL) {
  427. $result = array();
  428. foreach ($updates as $key => $update) {
  429. if (is_array($update)) {
  430. // It is a sub array of updates yet, process and merge
  431. $result = array_merge($result, _l10n_update_prepare_updates($update, $projects, $languages));
  432. }
  433. elseif ((!$projects || in_array($update->project, $projects)) && (!$languages || in_array($update->language, $languages))) {
  434. $directory = variable_get('l10n_update_download_store', '');
  435. if ($directory && empty($update->uri)) {
  436. // If we have a destination folder set just if we have no uri
  437. if (empty($update->uri)) {
  438. $update->uri = $directory . '/' . $update->filename;
  439. $update->keep = TRUE;
  440. }
  441. }
  442. $result[] = $update;
  443. }
  444. }
  445. return $result;
  446. }
  447. /**
  448. * Language refresh. Runs a batch for loading the selected languages.
  449. *
  450. * To be used after adding a new language.
  451. *
  452. * @param $languages
  453. * Array of language codes to check and download.
  454. */
  455. function l10n_update_language_refresh($languages) {
  456. $projects = l10n_update_get_projects();
  457. if ($available = l10n_update_check_projects($projects, $languages)) {
  458. $history = l10n_update_get_history();
  459. if ($updates = l10n_update_build_updates($history, $available)) {
  460. module_load_include('batch.inc', 'l10n_update');
  461. // Filter out updates in other languages. If no languages, all of them will be updated
  462. $updates = _l10n_update_prepare_updates($updates);
  463. $batch = l10n_update_batch_multiple($updates, variable_get('l10n_update_import_mode', LOCALE_IMPORT_KEEP));
  464. batch_set($batch);
  465. }
  466. }
  467. }