views_data_export_plugin_display_export.inc 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. <?php
  2. /**
  3. * @file
  4. * Contains the bulk export display plugin.
  5. *
  6. * This allows views to be rendered in parts by batch API.
  7. */
  8. /**
  9. * The plugin that batches its rendering.
  10. *
  11. * We are based on a feed display for compatibility.
  12. *
  13. * @ingroup views_display_plugins
  14. */
  15. class views_data_export_plugin_display_export extends views_plugin_display_feed {
  16. /**
  17. * The batched execution state of the view.
  18. */
  19. public $batched_execution_state;
  20. /**
  21. * The alias of the weight field in the index table.
  22. */
  23. var $weight_field_alias = '';
  24. /**
  25. * A map of the index column names to the expected views aliases.
  26. */
  27. var $field_aliases = array();
  28. /**
  29. * Private variable that stores the filename to save the results to.
  30. */
  31. var $_output_file = '';
  32. var $views_data_export_cached_view_loaded;
  33. var $errors = array();
  34. /**
  35. * Return the type of styles we require.
  36. */
  37. function get_style_type() { return 'data_export'; }
  38. /**
  39. * Return the sections that can be defaultable.
  40. */
  41. function defaultable_sections($section = NULL) {
  42. if (in_array($section, array('items_per_page', 'offset', 'use_pager', 'pager_element',))) {
  43. return FALSE;
  44. }
  45. return parent::defaultable_sections($section);
  46. }
  47. /**
  48. * Define the option for this view.
  49. */
  50. function option_definition() {
  51. $options = parent::option_definition();
  52. $options['use_batch'] = array('default' => 'no_batch');
  53. $options['items_per_page'] = array('default' => '0');
  54. $options['return_path'] = array('default' => '');
  55. $options['style_plugin']['default'] = 'views_data_export_csv';
  56. // This is the default size of a segment when doing a batched export.
  57. $options['segment_size']['default'] = 100;
  58. if (isset($options['defaults']['default']['items_per_page'])) {
  59. $options['defaults']['default']['items_per_page'] = FALSE;
  60. }
  61. return $options;
  62. }
  63. /**
  64. * Provide the summary for page options in the views UI.
  65. *
  66. * This output is returned as an array.
  67. */
  68. function options_summary(&$categories, &$options) {
  69. // It is very important to call the parent function here:
  70. parent::options_summary($categories, $options);
  71. $categories['page']['title'] = t('Data export settings');
  72. $options['use_batch'] = array(
  73. 'category' => 'page',
  74. 'title' => t('Batched export'),
  75. 'value' => ($this->get_option('use_batch') == 'batch' ? t('Yes') : t('No')),
  76. );
  77. if (!$this->is_compatible() && $this->get_option('use_batch')) {
  78. $options['use_batch']['value'] .= ' <strong>' . t('(Warning: incompatible)') . '</strong>';
  79. }
  80. }
  81. /**
  82. * Provide the default form for setting options.
  83. */
  84. function options_form(&$form, &$form_state) {
  85. // It is very important to call the parent function here:
  86. parent::options_form($form, $form_state);
  87. switch ($form_state['section']) {
  88. case 'use_batch':
  89. $form['#title'] .= t('Batched export');
  90. $form['use_batch'] = array(
  91. '#type' => 'radios',
  92. '#description' => t(''),
  93. '#default_value' => $this->get_option('use_batch'),
  94. '#options' => array(
  95. 'no_batch' => t('Export data all in one segment. Possible time and memory limit issues.'),
  96. 'batch' => t('Export data in small segments to build a complete export. Recommended for large exports sets (1000+ rows)'),
  97. ),
  98. );
  99. // Allow the administrator to configure the number of items exported per batch.
  100. $form['segment_size'] = array(
  101. '#type' => 'select',
  102. '#title' => t('Segment size'),
  103. '#description' => t('If each row of your export consumes a lot of memory to render, then reduce this value. Higher values will generally mean that the export completes in less time but will have a higher peak memory usage.'),
  104. '#options' => drupal_map_assoc(range(1, 500)),
  105. '#default_value' => $this->get_option('segment_size'),
  106. '#process' => array('ctools_dependent_process'),
  107. '#dependency' => array(
  108. 'radio:use_batch' => array('batch')
  109. ),
  110. );
  111. $form['return_path'] = array(
  112. '#title' => t('Return path'),
  113. '#type' => 'textfield',
  114. '#description' => t('Return path after the batched operation, leave empty for default. This path will only be used if the export URL is visited directly, and not by following a link when attached to another view display.'),
  115. '#default_value' => $this->get_option('return_path'),
  116. '#dependency' => array(
  117. 'radio:use_batch' => array('batch')
  118. ),
  119. );
  120. if (!$this->is_compatible()) {
  121. $form['use_batch']['#disabled'] = TRUE;
  122. $form['use_batch']['#default_value'] = 'no_batch';
  123. $form['use_batch']['message'] = array (
  124. '#type' => 'markup',
  125. '#markup' => theme('views_data_export_message', array('message' => t('The underlying database (!db_driver) is incompatible with the batched export option and it has been disabled.', array('!db_driver' => $this->_get_database_driver())), 'type' => 'warning')),
  126. '#weight' => -10,
  127. );
  128. }
  129. break;
  130. case 'cache':
  131. // We're basically going to disable using cache plugins, by disabling
  132. // the UI.
  133. if (isset($form['cache']['type']['#options'])) {
  134. foreach ($form['cache']['type']['#options'] as $id => $v) {
  135. if ($id != 'none') {
  136. unset($form['cache']['type']['#options'][$id]);
  137. }
  138. $form['cache']['type']['#description'] = t("Views data export isn't currently compatible with caching plugins.");
  139. }
  140. }
  141. break;
  142. }
  143. }
  144. function get_option($option) {
  145. // Force people to never use caching with Views data export. Sorry folks,
  146. // but it causes too many issues for our workflow. If you really want to add
  147. // caching back, then you can subclass this display handler and override
  148. // this method to add it back.
  149. if ($option == 'cache') {
  150. return array('type' => 'none');
  151. }
  152. return parent::get_option($option);
  153. }
  154. /**
  155. * Save the options from the options form.
  156. */
  157. function options_submit(&$form, &$form_state) {
  158. // It is very important to call the parent function here:
  159. parent::options_submit($form, $form_state);
  160. switch ($form_state['section']) {
  161. case 'use_batch':
  162. $this->set_option('use_batch', $form_state['values']['use_batch']);
  163. $this->set_option('segment_size', $form_state['values']['segment_size']);
  164. $this->set_option('return_path', $form_state['values']['return_path']);
  165. break;
  166. }
  167. }
  168. /**
  169. * Determine if this view should run as a batch or not.
  170. */
  171. function is_batched() {
  172. // The source of this option may change in the future.
  173. return ($this->get_option('use_batch') == 'batch') && empty($this->view->live_preview);
  174. }
  175. /**
  176. * Add HTTP headers for the file export.
  177. */
  178. function add_http_headers() {
  179. // Ask the style plugin to add any HTTP headers if it wants.
  180. if (method_exists($this->view->style_plugin, 'add_http_headers')) {
  181. $this->view->style_plugin->add_http_headers();
  182. }
  183. }
  184. /**
  185. * Execute this display handler.
  186. *
  187. * This is the main entry point for this display. We do different things based
  188. * on the stage in the rendering process.
  189. *
  190. * If we are being called for the very first time, the user has usually just
  191. * followed a link to our view. For this phase we:
  192. * - Register a new batched export with our parent module.
  193. * - Build and execute the view, redirecting the output into a temporary table.
  194. * - Set up the batch.
  195. *
  196. * If we are being called during batch processing we:
  197. * - Set up our variables from the context into the display.
  198. * - Call the rendering layer.
  199. * - Return with the appropriate progress value for the batch.
  200. *
  201. * If we are being called after the batch has completed we:
  202. * - Remove the index table.
  203. * - Show the complete page with a download link.
  204. * - Transfer the file if the download link was clicked.
  205. */
  206. function execute() {
  207. if (!$this->is_batched()) {
  208. return parent::execute();
  209. }
  210. // Try and get a batch context if possible.
  211. if (!empty($_GET['eid']) && !empty($_GET['token']) && drupal_valid_token($_GET['token'], 'views_data_export/' . $_GET['eid'])) {
  212. $eid = $_GET['eid'];
  213. }
  214. elseif (!empty($this->batched_execution_state->eid)) {
  215. $eid = $this->batched_execution_state->eid;
  216. }
  217. else {
  218. $eid = FALSE;
  219. }
  220. if ($eid) {
  221. $this->batched_execution_state = views_data_export_get($eid);
  222. }
  223. // First time through
  224. if (empty($this->batched_execution_state)) {
  225. $output = $this->execute_initial();
  226. }
  227. // Call me on the cached version of this view please
  228. // This allows this view to be programatically executed with nothing
  229. // more than the eid in $_GET in order for it to execute the next chunk
  230. // TODO: What is going on here?
  231. /*
  232. Jamsilver tells me this might be useful one day.
  233. if (!$this->views_data_export_cached_view_loaded) {
  234. $view = views_data_export_view_retrieve($this->batched_execution_state->eid);
  235. $view->set_display($this->view->current_display);
  236. $view->display_handler->batched_execution_state->eid = $this->batched_execution_state->eid;
  237. $view->display_handler->views_data_export_cached_view_loaded = TRUE;
  238. $ret = $view->execute_display($this->view->current_display);
  239. $this->batched_execution_state = &$view->display_handler->batched_execution_state;
  240. return $ret;
  241. }*/
  242. // Last time through
  243. if ($this->batched_execution_state->batch_state == VIEWS_DATA_EXPORT_FINISHED) {
  244. $output = $this->execute_final();
  245. }
  246. // In the middle of processing
  247. else {
  248. $output = $this->execute_normal();
  249. }
  250. //Ensure any changes we made to the database sandbox are saved
  251. views_data_export_update($this->batched_execution_state);
  252. return $output;
  253. }
  254. /**
  255. * Initializes the whole export process and starts off the batch process.
  256. *
  257. * Page execution will be ended at the end of this function.
  258. */
  259. function execute_initial() {
  260. // Register this export with our central table - get a unique eid
  261. // Also store our view in a cache to be retrieved with each batch call
  262. $this->batched_execution_state = views_data_export_new($this->view->name, $this->view->current_display, $this->outputfile_create());
  263. views_data_export_view_store($this->batched_execution_state->eid, $this->view);
  264. // Record a usage of our file, so we can identify our exports later.
  265. file_usage_add(file_load($this->batched_execution_state->fid), 'views_data_export', 'eid', $this->batched_execution_state->eid);
  266. // We need to build the index right now, before we lose $_GET etc.
  267. $this->initialize_index();
  268. //$this->batched_execution_state->fid = $this->outputfile_create();
  269. // Initialize the progress counter.
  270. if (db_table_exists($this->index_tablename())) {
  271. $this->batched_execution_state->sandbox['max'] = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
  272. }
  273. // Record the time we started.
  274. list($usec, $sec) = explode(' ', microtime());
  275. $this->batched_execution_state->sandbox['started'] = (float) $usec + (float) $sec;
  276. // Pop something into the session to ensure it stays aorund.
  277. $_SESSION['views_data_export'][$this->batched_execution_state->eid] = TRUE;
  278. // Build up our querystring for the final page callback.
  279. $querystring = array(
  280. 'eid' => $this->batched_execution_state->eid,
  281. 'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
  282. 'return-url' => NULL,
  283. );
  284. // If we have a configured return path, use that.
  285. if ($this->get_option('return_path')) {
  286. $querystring['return-url'] = $this->get_option('return_path');
  287. }
  288. // Else if we were attached to another view, grab that for the final URL.
  289. else if (!empty($_GET['attach']) && isset($this->view->display[$_GET['attach']])) {
  290. // Get the path of the attached display:
  291. $querystring['return-url'] = $this->view->get_url(NULL, $this->view->display[$_GET['attach']]->handler->get_path());
  292. }
  293. //Set the batch off
  294. $batch = array(
  295. 'operations' => array (
  296. array('_views_data_export_batch_process', array($this->batched_execution_state->eid, $this->view->current_display, $this->view->get_exposed_input())),
  297. ),
  298. 'title' => t('Building export'),
  299. 'init_message' => t('Export is starting up.'),
  300. 'progress_message' => t('Exporting @percentage% complete,'),
  301. 'error_message' => t('Export has encountered an error.'),
  302. );
  303. // We do not return, so update database sandbox now
  304. views_data_export_update($this->batched_execution_state);
  305. $final_destination = $this->view->get_url();
  306. // Provide a way in for others at this point
  307. // e.g. Drush to grab this batch and yet execute it in
  308. // it's own special way
  309. $batch['view_name'] = $this->view->name;
  310. $batch['exposed_filters'] = $this->view->get_exposed_input();
  311. $batch['display_id'] = $this->view->current_display;
  312. $batch['eid'] = $this->batched_execution_state->eid;
  313. $batch_redirect = array($final_destination, array('query' => $querystring));
  314. drupal_alter('views_data_export_batch', $batch, $batch_redirect);
  315. // Modules may have cleared out $batch, indicating that we shouldn't process further.
  316. if (!empty($batch)) {
  317. batch_set($batch);
  318. // batch_process exits
  319. batch_process($batch_redirect);
  320. }
  321. }
  322. /**
  323. * Compiles the next chunk of the output file
  324. */
  325. function execute_normal() {
  326. // Pass through to our render method,
  327. $output = $this->view->render();
  328. // Append what was rendered to the output file.
  329. $this->outputfile_write($output);
  330. // Store for convenience.
  331. $state = &$this->batched_execution_state;
  332. $sandbox = &$state->sandbox;
  333. // Update progress measurements & move our state forward
  334. switch ($state->batch_state) {
  335. case VIEWS_DATA_EXPORT_BODY:
  336. // Remove rendered results from our index
  337. if (count($this->view->result) && ($sandbox['weight_field_alias'])) {
  338. $last = end($this->view->result);
  339. db_delete($this->index_tablename())
  340. ->condition($sandbox['weight_field_alias'], $last->{$sandbox['weight_field_alias']}, '<=')
  341. ->execute();
  342. // Update progress.
  343. $progress = db_query('SELECT COUNT(*) FROM {' . $this->index_tablename() . '}')->fetchField();
  344. // TODO: These next few lines are messy, clean them up.
  345. $progress = 0.99 - ($progress / $sandbox['max'] * 0.99);
  346. $progress = ((int)floor($progress * 100000));
  347. $progress = $progress / 100000;
  348. $sandbox['finished'] = $progress;
  349. }
  350. else {
  351. // No more results.
  352. $progress = 0.99;
  353. $state->batch_state = VIEWS_DATA_EXPORT_FOOTER;
  354. }
  355. break;
  356. case VIEWS_DATA_EXPORT_HEADER:
  357. $sandbox['finished'] = 0;
  358. $state->batch_state = VIEWS_DATA_EXPORT_BODY;
  359. break;
  360. case VIEWS_DATA_EXPORT_FOOTER:
  361. // Update the temporary file size, otherwise we would get a problematic
  362. // "Content-Length: 0" HTTP header, that may break the export download.
  363. $this->outputfile_update_size();
  364. $sandbox['finished'] = 1;
  365. $state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
  366. break;
  367. }
  368. // Create a more helpful exporting message.
  369. $sandbox['message'] = $this->compute_time_remaining($sandbox['started'], $sandbox['finished']);
  370. }
  371. /**
  372. * Renders the final page
  373. * We should be free of the batch at this point
  374. */
  375. function execute_final() {
  376. // Should we download the file.
  377. if (!empty($_GET['download'])) {
  378. // Clean up our session, if we need to.
  379. if (isset($_SESSION)) {
  380. unset($_SESSION['views_data_export'][$this->batched_execution_state->eid]);
  381. if (empty($_SESSION['views_data_export'])) {
  382. unset($_SESSION['views_data_export']);
  383. }
  384. }
  385. // This next method will exit.
  386. $this->transfer_file();
  387. }
  388. else {
  389. // Remove the index table.
  390. $this->remove_index();
  391. return $this->render_complete();
  392. }
  393. }
  394. /**
  395. * Render the display.
  396. *
  397. * We basically just work out if we should be rendering the header, body or
  398. * footer and call the appropriate functions on the style plugins.
  399. */
  400. function render() {
  401. if (!$this->is_batched()) {
  402. $result = parent::render();
  403. if (empty($this->view->live_preview)) {
  404. $this->add_http_headers();
  405. }
  406. return $result;
  407. }
  408. $this->view->build();
  409. switch ($this->batched_execution_state->batch_state) {
  410. case VIEWS_DATA_EXPORT_BODY:
  411. $output = $this->view->style_plugin->render_body();
  412. break;
  413. case VIEWS_DATA_EXPORT_HEADER:
  414. $output = $this->view->style_plugin->render_header();
  415. break;
  416. case VIEWS_DATA_EXPORT_FOOTER:
  417. $output = $this->view->style_plugin->render_footer();
  418. break;
  419. }
  420. return $output;
  421. }
  422. /**
  423. * Trick views into thinking that we have executed the query and got results.
  424. *
  425. * We are called in the build phase of the view, but short circuit straight to
  426. * getting the results and making the view think it has already executed the
  427. * query.
  428. */
  429. function query() {
  430. if (!$this->is_batched()) {
  431. return parent::query();
  432. }
  433. // Make the query distinct if the option was set.
  434. if ($this->get_option('distinct')) {
  435. $this->view->query->set_distinct();
  436. }
  437. if (!empty($this->batched_execution_state->batch_state) && !empty($this->batched_execution_state->sandbox['weight_field_alias'])) {
  438. switch ($this->batched_execution_state->batch_state) {
  439. case VIEWS_DATA_EXPORT_BODY:
  440. case VIEWS_DATA_EXPORT_HEADER:
  441. case VIEWS_DATA_EXPORT_FOOTER:
  442. // Tell views its been executed.
  443. $this->view->executed = TRUE;
  444. // Grab our results from the index, and push them into the view result.
  445. // TODO: Handle external databases.
  446. $result = db_query_range('SELECT * FROM {' . $this->index_tablename() . '} ORDER BY ' . $this->batched_execution_state->sandbox['weight_field_alias'] . ' ASC', 0, $this->get_option('segment_size'));
  447. $this->view->result = array();
  448. $query_plugin = get_class($this->view->query);
  449. if ($query_plugin == 'views_plugin_query_default') {
  450. foreach ($result as $item_hashed) {
  451. $item = new stdClass();
  452. // We had to shorten some of the column names in the index, restore
  453. // those now.
  454. foreach ($item_hashed as $hash => $value) {
  455. if (isset($this->batched_execution_state->sandbox['field_aliases'][$hash])) {
  456. $item->{$this->batched_execution_state->sandbox['field_aliases'][$hash]} = $value;
  457. }
  458. else {
  459. $item->{$hash} = $value;
  460. }
  461. }
  462. // Push the restored $item in the views result array.
  463. $this->view->result[] = $item;
  464. }
  465. }
  466. elseif ($query_plugin == 'SearchApiViewsQuery') {
  467. foreach ($result as $row) {
  468. $item = unserialize($row->data);
  469. $item->{$this->batched_execution_state->sandbox['weight_field_alias']} = $row->{$this->batched_execution_state->sandbox['weight_field_alias']};
  470. $this->view->result[] = $item;
  471. }
  472. }
  473. $this->view->_post_execute();
  474. break;
  475. }
  476. }
  477. }
  478. /**
  479. * Render the 'Export Finished' page with the link to the file on it.
  480. */
  481. function render_complete() {
  482. $return_path = empty($_GET['return-url']) ? '' : $_GET['return-url'];
  483. $query = array(
  484. 'download' => 1,
  485. 'eid' => $this->batched_execution_state->eid,
  486. 'token' => drupal_get_token('views_data_export/' . $this->batched_execution_state->eid),
  487. );
  488. return theme('views_data_export_complete_page', array(
  489. 'file' => url($this->view->get_url(), array('query' => $query)),
  490. 'errors' => $this->errors,
  491. 'return_url' => $return_path));
  492. }
  493. /**
  494. * TBD - What does 'preview' mean for bulk exports?
  495. * According to doc:
  496. * "Fully render the display for the purposes of a live preview or
  497. * some other AJAXy reason. [views_plugin_display.inc:1877]"
  498. *
  499. * Not sure it makes sense for Bulk exports to be previewed in this manner?
  500. * We need the user's full attention to run the batch. Suggestions:
  501. * 1) Provide a link to execute the view?
  502. * 2) Provide a link to the last file we generated??
  503. * 3) Show a table of the first 20 results?
  504. */
  505. function preview() {
  506. if (!$this->is_batched()) {
  507. // Can replace with return parent::preview() when views 2.12 lands.
  508. if (!empty($this->view->live_preview)) {
  509. // Change the items per page.
  510. $this->view->set_items_per_page(20);
  511. // Force a pager to be used.
  512. $this->set_option('pager', array('type' => 'some', 'options' => array()));
  513. return '<p>' . t('A maximum of 20 items will be shown here, all results will be shown on export.') . '</p><pre>' . check_plain($this->view->render()) . '</pre>';
  514. }
  515. return $this->view->render();
  516. }
  517. return '';
  518. }
  519. /**
  520. * Transfer the output file to the client.
  521. */
  522. function transfer_file() {
  523. // Build the view so we can set the headers.
  524. $this->view->build();
  525. // Arguments can cause the style to not get built.
  526. if (!$this->view->init_style()) {
  527. $this->view->build_info['fail'] = TRUE;
  528. }
  529. // Set the headers.
  530. $this->add_http_headers();
  531. $headers = array(
  532. 'Content-Length' => $this->outputfile_entity()->filesize,
  533. );
  534. file_transfer($this->outputfile_path(), $headers);
  535. }
  536. /**
  537. * Called on export initialization.
  538. *
  539. * Modifies the view query to insert the results into a table, which we call
  540. * the 'index', this means we essentially have a snapshot of the results,
  541. * which we can then take time over rendering.
  542. *
  543. * This method is essentially all the best bits of the view::execute() method.
  544. */
  545. protected function initialize_index() {
  546. $view = &$this->view;
  547. // Get views to build the query.
  548. $view->build();
  549. $query_plugin = get_class($view->query);
  550. if ($query_plugin == 'views_plugin_query_default') {
  551. // Change the query object to use our custom one.
  552. switch ($this->_get_database_driver()) {
  553. case 'pgsql':
  554. $query_class = 'views_data_export_plugin_query_pgsql_batched';
  555. break;
  556. default:
  557. $query_class = 'views_data_export_plugin_query_default_batched';
  558. break;
  559. }
  560. $query = new $query_class();
  561. // Copy the query over:
  562. foreach ($view->query as $property => $value) {
  563. $query->$property = $value;
  564. }
  565. // Replace the query object.
  566. $view->query = $query;
  567. $view->execute();
  568. }
  569. elseif ($query_plugin == 'SearchApiViewsQuery') {
  570. $this->store_search_api_result(clone($view));
  571. }
  572. }
  573. /**
  574. * Given a view, construct an map of hashed aliases to aliases.
  575. *
  576. * The keys of the returned array will have a maximum length of 33 characters.
  577. */
  578. function field_aliases_create(&$view) {
  579. $all_aliases = array();
  580. foreach ($view->query->fields as $field) {
  581. if (strlen($field['alias']) > 32) {
  582. $all_aliases['a' . md5($field['alias'])] = $field['alias'];
  583. }
  584. else {
  585. $all_aliases[$field['alias']] = $field['alias'];
  586. }
  587. }
  588. return $all_aliases;
  589. }
  590. /**
  591. * Create an alias for the weight field in the index.
  592. *
  593. * This method ensures that it isn't the same as any other alias in the
  594. * supplied view's fields.
  595. */
  596. function _weight_alias_create(&$view) {
  597. $alias = 'vde_weight';
  598. $all_aliases = array();
  599. foreach ($view->query->fields as $field) {
  600. $all_aliases[] = $field['alias'];
  601. }
  602. // Keep appending '_' until we are unique.
  603. while (in_array($alias, $all_aliases)) {
  604. $alias .= '_';
  605. }
  606. return $alias;
  607. }
  608. /**
  609. * Remove the index.
  610. */
  611. function remove_index() {
  612. $ret = array();
  613. if (db_table_exists($this->index_tablename())) {
  614. db_drop_table($this->index_tablename());
  615. }
  616. }
  617. /**
  618. * Return the name of the unique table to store the index in.
  619. */
  620. function index_tablename() {
  621. return VIEWS_DATA_EXPORT_INDEX_TABLE_PREFIX . $this->batched_execution_state->eid;
  622. }
  623. /**
  624. * Get the output file entity.
  625. */
  626. public function outputfile_entity() {
  627. if (empty($this->_output_file)) {
  628. if (!empty($this->batched_execution_state->fid)) {
  629. // Return the filename associated with this file.
  630. $this->_output_file = $this->file_load($this->batched_execution_state->fid);
  631. }
  632. else {
  633. return NULL;
  634. }
  635. }
  636. return $this->_output_file;
  637. }
  638. /**
  639. * Get the output file path.
  640. */
  641. public function outputfile_path() {
  642. if ($file = $this->outputfile_entity()) {
  643. return $file->uri;
  644. }
  645. }
  646. /**
  647. * Called on export initialization
  648. * Creates the output file, registers it as a temporary file with Drupal
  649. * and returns the fid
  650. */
  651. protected function outputfile_create() {
  652. $dir = variable_get('views_data_export_directory', 'temporary://views_plugin_display');
  653. // Make sure the directory exists first.
  654. if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
  655. $this->abort_export(t('Could not create temporary directory for result export (@dir). Check permissions.', array ('@dir' => $dir)));
  656. }
  657. $path = drupal_tempnam($dir, 'views_data_export');
  658. // Save the file into the DB.
  659. $file = $this->file_save_file($path);
  660. // Make sure the file is marked as temporary.
  661. // There is no FILE_STATUS_TEMPORARY constant.
  662. $file->status = 0;
  663. file_save($file);
  664. return $file->fid;
  665. }
  666. /**
  667. * Write to the output file.
  668. */
  669. protected function outputfile_write($string) {
  670. $output_file = $this->outputfile_path();
  671. if (file_put_contents($output_file, $string, FILE_APPEND) === FALSE) {
  672. $this->abort_export(t('Could not write to temporary output file for result export (@file). Check permissions.', array ('@file' => $output_file)));
  673. }
  674. }
  675. /**
  676. * Updates the file size in the file entity.
  677. */
  678. protected function outputfile_update_size() {
  679. if ($file = $this->outputfile_entity()) {
  680. $file->filesize = filesize($file->uri);
  681. file_save($file);
  682. }
  683. }
  684. function abort_export($errors) {
  685. // Just cause the next batch to do the clean-up
  686. if (!is_array($errors)) {
  687. $errors = array($errors);
  688. }
  689. foreach ($errors as $error) {
  690. drupal_set_message($error . ' ['. t('Export Aborted') . ']', 'error');
  691. }
  692. $this->batched_execution_state->batch_state = VIEWS_DATA_EXPORT_FINISHED;
  693. }
  694. /**
  695. * Load a file from the database.
  696. *
  697. * @param $fid
  698. * A numeric file id or string containing the file path.
  699. * @return
  700. * A file object.
  701. */
  702. function file_load($fid) {
  703. return file_load($fid);
  704. }
  705. /**
  706. * Save a file into a file node after running all the associated validators.
  707. *
  708. * This function is usually used to move a file from the temporary file
  709. * directory to a permanent location. It may be used by import scripts or other
  710. * modules that want to save an existing file into the database.
  711. *
  712. * @param $filepath
  713. * The local file path of the file to be saved.
  714. * @return
  715. * An array containing the file information, or 0 in the event of an error.
  716. */
  717. function file_save_file($filepath) {
  718. return file_save_data('', $filepath, FILE_EXISTS_REPLACE);
  719. }
  720. /**
  721. * Helper function that computes the time remaining
  722. */
  723. function compute_time_remaining($started, $finished) {
  724. list($usec, $sec) = explode(' ', microtime());
  725. $now = (float) $usec + (float) $sec;
  726. $diff = round(($now - $started), 0);
  727. // So we've taken $diff seconds to get this far.
  728. if ($finished > 0) {
  729. $estimate_total = $diff / $finished;
  730. $stamp = max(1, $estimate_total - $diff);
  731. // Round up to nearest 30 seconds.
  732. $stamp = ceil($stamp / 30) * 30;
  733. // Set the message in the batch context.
  734. return t('Time remaining: about @interval.', array('@interval' => format_interval($stamp)));
  735. }
  736. }
  737. /**
  738. * Checks the driver of the database underlying
  739. * this query and returns FALSE if it is imcompatible
  740. * with the approach taken in this display.
  741. * Basically mysql & mysqli will be fine, pg will not
  742. */
  743. function is_compatible() {
  744. $incompatible_drivers = array (
  745. //'pgsql',
  746. );
  747. $db_driver = $this->_get_database_driver();
  748. return !in_array($db_driver, $incompatible_drivers);
  749. }
  750. function _get_database_driver() {
  751. $name = !empty($this->view->base_database) ? $this->view->base_database : 'default';
  752. $conn_info = Database::getConnectionInfo($name);
  753. return $conn_info['default']['driver'];
  754. }
  755. /**
  756. * Based on views_data_export_plugin_query_default_batched::execute().
  757. */
  758. function store_search_api_result($view) {
  759. $display_handler = &$view->display_handler;
  760. $start = microtime(TRUE);
  761. try {
  762. // Get all the view results.
  763. $view->query->set_limit(NULL);
  764. $view->query->set_offset(0);
  765. $view->query->execute($view);
  766. $weight_alias = 'vde_weight';
  767. $display_handler->batched_execution_state->sandbox['weight_field_alias'] = $weight_alias;
  768. $schema = array(
  769. 'fields' => array(
  770. $weight_alias => array('type' => 'int'),
  771. 'data' => array('type' => 'blob'),
  772. ));
  773. db_create_table($display_handler->index_tablename(), $schema);
  774. if (!empty($view->result)) {
  775. $insert_query = db_insert($display_handler->index_tablename())->fields(array($weight_alias, 'data'));
  776. $weight = 0;
  777. foreach ($view->result as $item) {
  778. $insert_query->values(array(
  779. $weight_alias => $weight,
  780. 'data' => serialize($item),
  781. ));
  782. $weight++;
  783. }
  784. $insert_query->execute();
  785. }
  786. $view->result = array();
  787. // Now create an index for the weight field, otherwise the queries on the
  788. // index will take a long time to execute.
  789. db_add_unique_key($display_handler->index_tablename(), $weight_alias, array($weight_alias));
  790. }
  791. catch (Exception $e) {
  792. $view->result = array();
  793. debug('Exception: ' . $e->getMessage());
  794. }
  795. $view->execute_time = microtime(TRUE) - $start;
  796. }
  797. }
  798. class views_data_export_plugin_query_default_batched extends views_plugin_query_default {
  799. /**
  800. * Executes the query and fills the associated view object with according
  801. * values.
  802. *
  803. * Values to set: $view->result, $view->total_rows, $view->execute_time,
  804. * $view->current_page.
  805. */
  806. function execute(&$view) {
  807. $display_handler = &$view->display_handler;
  808. $external = FALSE; // Whether this query will run against an external database.
  809. $query = $view->build_info['query'];
  810. $count_query = $view->build_info['count_query'];
  811. $query->addMetaData('view', $view);
  812. $count_query->addMetaData('view', $view);
  813. if (empty($this->options['disable_sql_rewrite'])) {
  814. $base_table_data = views_fetch_data($this->base_table);
  815. if (isset($base_table_data['table']['base']['access query tag'])) {
  816. $access_tag = $base_table_data['table']['base']['access query tag'];
  817. $query->addTag($access_tag);
  818. $count_query->addTag($access_tag);
  819. }
  820. }
  821. $items = array();
  822. if ($query) {
  823. $additional_arguments = module_invoke_all('views_query_substitutions', $view);
  824. // Count queries must be run through the preExecute() method.
  825. // If not, then hook_query_node_access_alter() may munge the count by
  826. // adding a distinct against an empty query string
  827. // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
  828. // See pager.inc > PagerDefault::execute()
  829. // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
  830. // See http://drupal.org/node/1046170.
  831. $count_query->preExecute();
  832. // Build the count query.
  833. $count_query = $count_query->countQuery();
  834. // Add additional arguments as a fake condition.
  835. // XXX: this doesn't work... because PDO mandates that all bound arguments
  836. // are used on the query. TODO: Find a better way to do this.
  837. if (!empty($additional_arguments)) {
  838. // $query->where('1 = 1', $additional_arguments);
  839. // $count_query->where('1 = 1', $additional_arguments);
  840. }
  841. $start = microtime(TRUE);
  842. if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
  843. $this->pager->execute_count_query($count_query);
  844. }
  845. // Let the pager modify the query to add limits.
  846. $this->pager->pre_execute($query);
  847. if (!empty($this->limit) || !empty($this->offset)) {
  848. // We can't have an offset without a limit, so provide a very large limit instead.
  849. $limit = intval(!empty($this->limit) ? $this->limit : 999999);
  850. $offset = intval(!empty($this->offset) ? $this->offset : 0);
  851. $query->range($offset, $limit);
  852. }
  853. try {
  854. // The $query is final and ready to go, we are going to redirect it to
  855. // become an insert into our table, sneaky!
  856. // Our query will look like:
  857. // CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
  858. // (-query-) AS cl, (SELECT @row := 0) AS r
  859. // We do some magic to get the row count.
  860. $display_handler->batched_execution_state->sandbox['weight_field_alias'] = $display_handler->_weight_alias_create($view);
  861. $display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
  862. $select_aliases = array();
  863. foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
  864. $select_aliases[] = "cl.$alias AS $hash";
  865. }
  866. // TODO: this could probably be replaced with a query extender and new query type.
  867. $query->preExecute();
  868. $args = $query->getArguments();
  869. $insert_query = 'CREATE TABLE {' . $display_handler->index_tablename() . '} SELECT @row := @row + 1 AS ' . $display_handler->batched_execution_state->sandbox['weight_field_alias'] . ', ' . implode(', ', $select_aliases) . ' FROM (' . (string)$query . ') AS cl, (SELECT @row := 0) AS r';
  870. db_query($insert_query, $args);
  871. $view->result = array();
  872. $this->pager->post_execute($view->result);
  873. if ($this->pager->use_pager()) {
  874. $view->total_rows = $this->pager->get_total_items();
  875. }
  876. // Now create an index for the weight field, otherwise the queries on the
  877. // index will take a long time to execute.
  878. db_add_unique_key($display_handler->index_tablename(), $display_handler->batched_execution_state->sandbox['weight_field_alias'], array($display_handler->batched_execution_state->sandbox['weight_field_alias']));
  879. }
  880. catch (Exception $e) {
  881. $view->result = array();
  882. debug('Exception: ' . $e->getMessage());
  883. }
  884. }
  885. $view->execute_time = microtime(TRUE) - $start;
  886. }
  887. }
  888. class views_data_export_plugin_query_pgsql_batched extends views_data_export_plugin_query_default_batched {
  889. /**
  890. * Executes the query and fills the associated view object with according
  891. * values.
  892. *
  893. * Values to set: $view->result, $view->total_rows, $view->execute_time,
  894. * $view->current_page.
  895. */
  896. function execute(&$view) {
  897. $display_handler = &$view->display_handler;
  898. $external = FALSE; // Whether this query will run against an external database.
  899. $query = $view->build_info['query'];
  900. $count_query = $view->build_info['count_query'];
  901. $query->addMetaData('view', $view);
  902. $count_query->addMetaData('view', $view);
  903. if (empty($this->options['disable_sql_rewrite'])) {
  904. $base_table_data = views_fetch_data($this->base_table);
  905. if (isset($base_table_data['table']['base']['access query tag'])) {
  906. $access_tag = $base_table_data['table']['base']['access query tag'];
  907. $query->addTag($access_tag);
  908. $count_query->addTag($access_tag);
  909. }
  910. }
  911. $items = array();
  912. if ($query) {
  913. $additional_arguments = module_invoke_all('views_query_substitutions', $view);
  914. // Count queries must be run through the preExecute() method.
  915. // If not, then hook_query_node_access_alter() may munge the count by
  916. // adding a distinct against an empty query string
  917. // (e.g. COUNT DISTINCT(1) ...) and no pager will return.
  918. // See pager.inc > PagerDefault::execute()
  919. // http://api.drupal.org/api/drupal/includes--pager.inc/function/PagerDefault::execute/7
  920. // See http://drupal.org/node/1046170.
  921. $count_query->preExecute();
  922. // Build the count query.
  923. $count_query = $count_query->countQuery();
  924. // Add additional arguments as a fake condition.
  925. // XXX: this doesn't work... because PDO mandates that all bound arguments
  926. // are used on the query. TODO: Find a better way to do this.
  927. if (!empty($additional_arguments)) {
  928. // $query->where('1 = 1', $additional_arguments);
  929. // $count_query->where('1 = 1', $additional_arguments);
  930. }
  931. $start = microtime(TRUE);
  932. if ($this->pager->use_count_query() || !empty($view->get_total_rows)) {
  933. $this->pager->execute_count_query($count_query);
  934. }
  935. // Let the pager modify the query to add limits.
  936. $this->pager->pre_execute($query);
  937. if (!empty($this->limit) || !empty($this->offset)) {
  938. // We can't have an offset without a limit, so provide a very large limit instead.
  939. $limit = intval(!empty($this->limit) ? $this->limit : 999999);
  940. $offset = intval(!empty($this->offset) ? $this->offset : 0);
  941. $query->range($offset, $limit);
  942. }
  943. try {
  944. // The $query is final and ready to go, we are going to redirect it to
  945. // become an insert into our table, sneaky!
  946. // Our query will look like:
  947. // CREATE TABLE {idx} SELECT @row := @row + 1 AS weight_alias, cl.* FROM
  948. // (-query-) AS cl, (SELECT @row := 0) AS r
  949. // We do some magic to get the row count.
  950. $display_handler->batched_execution_state->sandbox['weight_field_alias'] = $display_handler->_weight_alias_create($view);
  951. $display_handler->batched_execution_state->sandbox['field_aliases'] = $display_handler->field_aliases_create($view);
  952. $select_aliases = array();
  953. foreach ($display_handler->batched_execution_state->sandbox['field_aliases'] as $hash => $alias) {
  954. $select_aliases[] = "cl.$alias AS $hash";
  955. }
  956. // TODO: this could probably be replaced with a query extender and new query type.
  957. $query->preExecute();
  958. $args = $query->getArguments();
  959. // Create temporary sequence
  960. $seq_name = $display_handler->index_tablename() . '_seq';
  961. db_query('CREATE TEMP sequence ' . $seq_name);
  962. // Query uses sequence to create row number
  963. $insert_query = 'CREATE TABLE {' . $display_handler->index_tablename() . "} AS SELECT nextval('". $seq_name . "') AS " . $display_handler->batched_execution_state->sandbox['weight_field_alias'] . ', ' . implode(', ', $select_aliases) . ' FROM (' . (string)$query . ') AS cl';
  964. db_query($insert_query, $args);
  965. $view->result = array();
  966. $this->pager->post_execute($view->result);
  967. if ($this->pager->use_pager()) {
  968. $view->total_rows = $this->pager->get_total_items();
  969. }
  970. // Now create an index for the weight field, otherwise the queries on the
  971. // index will take a long time to execute.
  972. db_add_unique_key($display_handler->index_tablename(), $display_handler->batched_execution_state->sandbox['weight_field_alias'], array($display_handler->batched_execution_state->sandbox['weight_field_alias']));
  973. }
  974. catch (Exception $e) {
  975. $view->result = array();
  976. debug('Exception: ' . $e->getMessage());
  977. }
  978. }
  979. $view->execute_time = microtime(TRUE) - $start;
  980. }
  981. }