l10n_update.check.inc 16 KB

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