From 90bddf2b92e283ecabdd71bd34f7fdd14492916c Mon Sep 17 00:00:00 2001 From: wizonesolutions <wizonesolutions@739994.no-reply.drupal.org> Date: Fri, 14 Dec 2018 23:33:47 +0100 Subject: [PATCH] Issue #1526452 by wizonesolutions, jlamp, cmcintosh, spiderman, newmediaist, Liam Morland, miksha, JordiTR: Support entities instead of nodes --- fillpdf.admin.inc | 50 ++- fillpdf.install | 22 +- fillpdf.module | 233 +++++++++++--- tests/FillPdfMergeTestCase.test | 315 ++++++++++++++++++- tests/FillPdfTestCase.test | 40 ++- tests/FillPdfTestHelper.test | 44 +++ tests/modules/fillpdf_test/fillpdf_test.info | 2 + 7 files changed, 640 insertions(+), 66 deletions(-) diff --git a/fillpdf.admin.inc b/fillpdf.admin.inc index ded4bc2..2e82c3e 100644 --- a/fillpdf.admin.inc +++ b/fillpdf.admin.inc @@ -349,14 +349,40 @@ function fillpdf_form_edit($form, &$form_state, $fid) { '#title' => 'Tokens', ) + _fillpdf_admin_token_form(TRUE); + $entity_mode = module_exists('entity_token'); $form['default_nid'] = array( '#type' => 'textfield', - '#title' => t('Default Node ID'), - '#description' => t('When filling a PDF, use this node for the data source if no node is specified in the FillPDF URL.'), + '#title' => $entity_mode ? t('Default Entity ID') : t('Default Node ID'), + '#description' => $entity_mode ? t('When filling a PDF, use this entity for the data source if none is specified in the FillPDF URL. (When filling from Webform data, this is treated as the Webform Node ID.)') : t('When filling a PDF, use this node for the data source if no node is specified in the FillPDF URL.'), '#maxlength' => 10, '#default_value' => $pdf_form->default_nid, ); + if ($entity_mode) { + // Compile a list of all entity types that we know provide tokens. + $entity_type_options = array(); + $entity_types = entity_get_info(); + foreach ($entity_types as $entity_type => $entity_info) { + if (!empty($entity_info['token type'])) { + $entity_type_options[$entity_type] = $entity_info['label']; + } + } + $form['default_entity_type'] = array( + '#type' => 'select', + '#title' => t('Default Entity Type'), + '#description' => t('When filling a PDF, treat the entity ID as this type if none is specified in the FillPDF URL. If this is not set, <em>Node</em> is assumed.'), + '#options' => array('' => t('- None -')) + $entity_type_options, + '#default_value' => $pdf_form->default_entity_type, + ); + } + else { + // Don't break the submit method. Just supply it blank. + $form['default_entity_type'] = array( + '#type' => 'value', + '#value' => '', + ); + } + // @todo: They can upload a PDF any time, but fields will only be generated on // add. Don't want to purge existing fields, however a user might have // accidently uploaded an old template and discover much later (if it's @@ -385,15 +411,15 @@ function fillpdf_form_edit($form, &$form_state, $fid) { if (!empty($pdf_form->default_nid)) { $form['pdf_info']['populate_default'] = array( '#type' => 'item', - '#title' => 'Fill PDF from default node', - '#description' => l(t('Download this PDF filled with data from the default node (@node).', array('@node' => $pdf_form->default_nid)), fillpdf_pdf_link($fid)) . '<br />' . + '#title' => $entity_mode ? t('Fill PDF from default entity') : t('Fill PDF from default node'), + '#description' => ($entity_mode ? l(t('Download this PDF filled with data from the default entity (@entity).', array('@entity' => $pdf_form->default_nid)), fillpdf_pdf_link($fid)) : l(t('Download this PDF filled with data from the default node (@node).', array('@node' => $pdf_form->default_nid)), fillpdf_pdf_link($fid))) . '<br />' . t('If you have set a custom path on this PDF, the sample will be saved there silently.'), ); } $form['pdf_info']['form_id'] = array( '#type' => 'item', '#title' => 'Form Info', - '#description' => "Form ID: [$fid]. Populate this form with node IDs, such as /fillpdf?fid=$fid&nid=10<br/>", + '#description' => t('Form ID: [@fid]. Populate this form with node IDs, such as @example<br>', array('@fid' => $fid, '@example' => $entity_mode ? "/fillpdf?fid=$fid&nid=10" : "/fillpdf?fid=$fid&entity_id=node:10")), ); $form['extra'] = array( '#type' => 'fieldset', @@ -516,7 +542,7 @@ function fillpdf_form_edit($form, &$form_state, $fid) { * */ function fillpdf_form_edit_validate($form, &$form_state) { - $file = $_FILES['files']['name']['upload_pdf']; + $file = isset($_FILES['files']['name']['upload_pdf']) ? $_FILES['files']['name']['upload_pdf'] : NULL; if ($file) { $validate_file = _fillpdf_validate_upload($file); if (isset($validate_file['#error'])) { @@ -528,7 +554,7 @@ function fillpdf_form_edit_validate($form, &$form_state) { $using_private_files = $scheme === 'private'; $destination_set = !empty($form_state['values']['destination_path']); $private_destination_path_is_absolute = $using_private_files && $destination_set && - substr($form_state['values']['destination_path'], 0, 1) === '/'; + strpos($form_state['values']['destination_path'], '/') === 0; if ($private_destination_path_is_absolute) { form_set_error('destination_path', t('You have chosen to use <em>Private files</em> for storage. Your destination @@ -554,10 +580,11 @@ function fillpdf_form_edit_submit($form, &$form_state) { 'destination_redirect' => $form_state['values']['destination_redirect'], 'admin_title' => $form_state['values']['admin_title'], 'scheme' => $form_state['values']['scheme'], + 'default_entity_type' => $form_state['values']['default_entity_type'], )) ->condition('fid', $form['#pdf_form']->fid) ->execute(); - $file = $_FILES['files']['name']['upload_pdf']; + $file = isset($_FILES['files']['name']['upload_pdf']) ? $_FILES['files']['name']['upload_pdf'] : NULL; if ($file) { // Export the current field mappings to a variable. $mappings = fillpdf_generate_mappings($form['#pdf_form'], TRUE); @@ -910,6 +937,13 @@ function _fillpdf_admin_token_form($dialog = FALSE) { 'site', ); + if (module_exists('entity_token')) { + $entities = entity_get_info(); + foreach ($entities as $entity => $info) { + $token_types[] = !empty($info['token type']) ? $info['token type'] : $entity; + } + } + // If not using Webform Rules, then show potential Webform Tokens // webform:-namespaced tokens. if (module_exists('webform_rules') === FALSE) { diff --git a/fillpdf.install b/fillpdf.install index 22dd59a..de5b525 100644 --- a/fillpdf.install +++ b/fillpdf.install @@ -24,8 +24,8 @@ function fillpdf_schema() { 'not null' => TRUE, ), 'default_nid' => array( - 'type' => 'int', - 'unsigned' => TRUE, + 'type' => 'varchar', + 'length' => 255, ), 'url' => array( 'type' => 'varchar', @@ -52,6 +52,10 @@ function fillpdf_schema() { 'type' => 'varchar', 'length' => 64, ), + 'default_entity_type' => array( + 'type' => 'varchar', + 'length' => 255, + ), ), 'primary key' => array('fid'), ); @@ -363,3 +367,17 @@ function _fillpdf_old_file_context_load($fcid) { return $json; } + +/** + * Add field to store default entity type. Also change default_nid to varchar. + */ +function fillpdf_update_7107() { + if (!db_field_exists('fillpdf_forms', 'default_entity_type')) { + db_add_field('fillpdf_forms', 'default_entity_type', array('type' => 'varchar', 'length' => 255)); + } + // Change default_nid into a varchar to support entities with string IDs. + db_change_field('fillpdf_forms', 'default_nid', 'default_nid', array( + 'type' => 'varchar', + 'length' => 255, + )); +} diff --git a/fillpdf.module b/fillpdf.module index f85ccd6..810a071 100644 --- a/fillpdf.module +++ b/fillpdf.module @@ -1,11 +1,6 @@ <?php -/** - * @file - * Allows mappings of PDFs to site content. - */ - -require_once dirname(__FILE__) . '/fillpdf.deprecated.inc'; +require_once __DIR__ . '/fillpdf.deprecated.inc'; define("FILLPDF_DEFAULT_SERVLET_URL", variable_get('fillpdf_remote_protocol', 'https') . "://" . variable_get('fillpdf_remote_endpoint', "fillpdf.io/xmlrpc.php")); module_load_include('inc', 'fillpdf', 'fillpdf.admin'); @@ -158,6 +153,8 @@ function fillpdf_modules_enabled($modules) { /** * Implements hook_file_download(). + * + * @todo: Rewrite this to not nest so deeply, if possible. It would be better to return early. */ function fillpdf_file_download($uri) { // Do we handle this kind of file? Check using string functions for @@ -203,7 +200,7 @@ function fillpdf_file_download($uri) { if ($stub_context['fid']) { // Expand the stub context (load the entities). $fillpdf_info = fillpdf_load($stub_context['fid']); - $file_context = fillpdf_load_entities($fillpdf_info, $stub_context['nids'], $stub_context['webforms'], $stub_context['uc_order_ids'], $stub_context['uc_order_product_ids'], $GLOBALS['user']); + $file_context = fillpdf_load_entities($fillpdf_info, $stub_context['nids'], $stub_context['webforms'], $stub_context['uc_order_ids'], $stub_context['uc_order_product_ids'], $GLOBALS['user'], $stub_context['entity_ids']); // Check access as if they were filling in the PDF from // scratch. @@ -254,12 +251,13 @@ function fillpdf_file_download_access_alter(&$grants, $file_item, $entity_type, * gist of PDF) * @param $uc_order_ids * @param $uc_order_product_ids + * @param $entity_ids * * @return string * The file url. */ -function fillpdf_pdf_link($fid, $nids = NULL, array $webform_arr = NULL, $sample = FALSE, $uc_order_ids = NULL, $uc_order_product_ids = NULL) { - $nids_uri = $webforms_uri = $uc_orders_uri = $uc_order_products_uri = ""; +function fillpdf_pdf_link($fid, $nids = NULL, array $webform_arr = NULL, $sample = FALSE, $uc_order_ids = NULL, $uc_order_product_ids = NULL, $entity_ids = NULL) { + $nids_uri = $webforms_uri = $uc_orders_uri = $uc_order_products_uri = $entity_ids_uri = ''; // If an integer was provided, recast into a single-element array. if ($nids && !is_array($nids)) { @@ -275,7 +273,7 @@ function fillpdf_pdf_link($fid, $nids = NULL, array $webform_arr = NULL, $sample } foreach ($webform_arr as $key => $webform) { $webforms_uri .= "&webforms[{$key}][nid]={$webform['nid']}"; - $webforms_uri .= isset($webform['sid']) ? "&webforms[{$key}][sid]={$webform['sid']}" : ""; + $webforms_uri .= isset($webform['sid']) ? "&webforms[{$key}][sid]={$webform['sid']}" : ''; } } @@ -293,10 +291,16 @@ function fillpdf_pdf_link($fid, $nids = NULL, array $webform_arr = NULL, $sample $uc_order_products_uri = "&uc_order_product_ids[]={$uc_order_product_ids}"; } + if (is_array($entity_ids) && count($entity_ids)) { + foreach ($entity_ids as $entity_id) { + $entity_ids_uri .= "&entity_ids[]={$entity_id}"; + } + } + $sample = $sample ? '&sample=true' : ''; // @todo: Refactor to use real url() syntax once tests in place - return url('', array('absolute' => TRUE)) . "fillpdf?fid={$fid}{$nids_uri}{$webforms_uri}{$uc_orders_uri}{$uc_order_products_uri}{$sample}"; + return url('', array('absolute' => TRUE)) . "fillpdf?fid={$fid}{$nids_uri}{$webforms_uri}{$uc_orders_uri}{$uc_order_products_uri}{$entity_ids_uri}{$sample}"; } /** @@ -314,7 +318,7 @@ function fillpdf_parse_uri($uri = NULL) { $context = fillpdf_link_to_stub_context($uri); - fillpdf_merge_pdf($context['fid'], $context['nids'], $context['webforms'], $context['sample'], $context['force_download'], FALSE, $context['flatten'], TRUE, $context['uc_order_ids'], $context['uc_order_product_ids']); + fillpdf_merge_pdf($context['fid'], $context['nids'], $context['webforms'], $context['sample'], $context['force_download'], FALSE, $context['flatten'], TRUE, $context['uc_order_ids'], $context['uc_order_product_ids'], $context['entity_ids']); } /** @@ -331,6 +335,7 @@ function fillpdf_link_to_stub_context($uri) { 'webforms' => array(), 'uc_order_ids' => array(), 'uc_order_product_ids' => array(), + 'entity_ids' => array(), 'force_download' => FALSE, 'flatten' => TRUE, 'sample' => FALSE, @@ -340,6 +345,8 @@ function fillpdf_link_to_stub_context($uri) { $query_string += array( 'nid' => NULL, 'nids' => NULL, + 'entity_id' => NULL, + 'entity_ids' => NULL, 'webform' => NULL, 'webforms' => NULL, 'uc_order_id' => NULL, @@ -371,10 +378,41 @@ function fillpdf_link_to_stub_context($uri) { $context['uc_order_product_ids'] = ($query_string['uc_order_product_id'] ? array($query_string['uc_order_product_id']) : $query_string['uc_order_product_ids']); } - if (isset($query_string['download']) && (int) $query_string['download'] == 1) { + // 'entities' and 'entity' are deprecated legacy formats from the original + // patch that added entity support. They're supported to help users, but they + // are not documented or official. + if ($query_string['entity_ids'] + || $query_string['entity_id'] + || isset($query_string['entity']) + || isset($query_string['entities'])) { + // Translate legacy format into modern format. For simplicity, this will + // overwrite the official parameter if they specify both. Specifying both is + // not a supported link format anyway. A single entity_id is also more + // powerful than entity_ids (this is consistent with the behavior of other + // query string parameters, and specifying both is also not a supported + // format). + if (isset($query_string['entities'])) { + $query_string['entity_ids'] = $query_string['entities']; + unset($query_string['entities']); + } + if (isset($query_string['entity'])) { + $query_string['entity_id'] = $query_string['entity']; + unset($query_string['entity']); + } + // If entity_type was specified, we assume that entity_id is just an integer + // and parse it as an entity of that type. + if (isset($query_string['entity_type'], $query_string['entity_id'])) { + $query_string['entity_id'] = "{$query_string['entity_type']}:{$query_string['entity_id']}"; + unset($query_string['entity_type']); + } + $context['entity_ids'] = isset($query_string['entity_id']) ? array($query_string['entity_id']) : $query_string['entity_ids']; + } + + if (isset($query_string['download']) && (int) $query_string['download'] === 1) { $context['force_download'] = TRUE; } - if (isset($query_string['flatten']) && (int) $query_string['flatten'] == 0) { + + if (isset($query_string['flatten']) && (int) $query_string['flatten'] === 0) { $context['flatten'] = FALSE; return $context; } @@ -383,6 +421,7 @@ function fillpdf_link_to_stub_context($uri) { /** * Translates a FillPDF context array into a link. + * @throws \EntityMalformedException */ function fillpdf_context_to_link($fid, $context, $sample = FALSE) { $nids = NULL; @@ -416,7 +455,17 @@ function fillpdf_context_to_link($fid, $context, $sample = FALSE) { } } - return fillpdf_pdf_link($fid, $nids, $webforms, $sample, $uc_orders, $uc_order_products); + $entity_ids = NULL; + if (!empty($context['entities'])) { + foreach ($context['entities'] as $entity_type => $entities_by_type) { + foreach ($entities_by_type as $entity) { + list($id) = entity_extract_ids($entity_type, $entity); + $entity_ids[] = $entity_type . ':' . $id; + } + } + } + + return fillpdf_pdf_link($fid, $nids, $webforms, $sample, $uc_orders, $uc_order_products, $entity_ids); } /** @@ -425,31 +474,37 @@ function fillpdf_context_to_link($fid, $context, $sample = FALSE) { * Constructs a page from scratch (pdf content-type) and sends it to the * browser or saves it, depending on if a custom path is configured or not. * - * @param $fid + * @param int $fid * The integer ID of the PDF. - * @param $nids - * Array of integer IDs of the CCK nodes from which to draw data. - * @param $webform_arr - * Array of integer IDs of the Webform nodes from which to draw data. - * @param $sample + * @param int[] $nids + * Array of integer IDs of the content (nodes) from which to draw data. + * @param array[] $webform_array + * Array of arrays, each containing 'nid' and 'sid' keys. The 'nid' key + * refers to the ID of the Webform node, and the 'sid' refers to the + * Webform submission ID. If no nid is supplied, the default ID from the + * FillPDF Form (if set) is used to try to load a node. + * @param bool $sample * If "true" (exact string), each field will be filled with its field name. - * @param $force_download + * @param bool $force_download * Boolean. If TRUE, always send a PDF to the browser, even if a * destination_path is set for the PDF. - * @param $skip_access_check + * @param bool $skip_access_check * Boolean. If TRUE, do not do any access checks. Allow the user to download * any PDF with data from any node. Only use when access checks are being * done some other way. - * @param $flatten + * @param bool $flatten * Boolean. If TRUE, flatten the PDF so that fields cannot be edited. * Otherwise leave fields editable. - * @param $handle + * @param bool $handle * Boolean. If TRUE, handle the PDF, which usually consists of sending it to * the users's browser or saving it as a file. - * @param $uc_order_ids + * @param int[] $uc_order_ids * Array of integer IDs of Ubercart orders from which to - * @param $uc_order_product_ids + * @param int[] $uc_order_product_ids * Array of integer IDs of Ubercart ordered products from which to draw data. + * @param array $entity_ids + * Array of IDs of entities from which to draw data. IDs may be integers or + * strings depending on the entity type. * * @return object * When $handle is FALSE, this function returns the variable it would have @@ -461,7 +516,7 @@ function fillpdf_context_to_link($fid, $context, $sample = FALSE) { * @todo: Refactor to take fewer arguments once tests in place. * MAYBE in FillPDF 3 - might not want to break backwards-compatibility. */ -function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = NULL, $force_download = FALSE, $skip_access_check = FALSE, $flatten = TRUE, $handle = TRUE, $uc_order_ids = NULL, $uc_order_product_ids = NULL) { +function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = NULL, $force_download = FALSE, $skip_access_check = FALSE, $flatten = TRUE, $handle = TRUE, $uc_order_ids = NULL, $uc_order_product_ids = NULL, $entity_ids = NULL) { // Case 1: No $fid. if (is_null($fid)) { drupal_set_message(t('FillPDF Form ID required to print a PDF.'), 'warning'); @@ -478,15 +533,16 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = } global $user; - $context = fillpdf_load_entities($fillpdf_info, $nids, $webform_array, $uc_order_ids, $uc_order_product_ids, $user); + $context = fillpdf_load_entities($fillpdf_info, $nids, $webform_array, $uc_order_ids, $uc_order_product_ids, $user, $entity_ids); $nodes = $context['nodes']; $webforms = $context['webforms']; $uc_orders = $context['uc_orders']; $uc_order_products = $context['uc_order_products']; + $entities = $context['entities']; if ($skip_access_check !== TRUE) { - if (!fillpdf_merge_pdf_access($nodes, $webforms, $uc_orders, $uc_order_products)) { + if (!fillpdf_merge_pdf_access($nodes, $webforms, $uc_orders, $uc_order_products, $entities)) { drupal_access_denied(); drupal_exit(); } @@ -513,6 +569,7 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = $webforms = array_reverse($webforms); $uc_orders = array_reverse($uc_orders); $uc_order_products = array_reverse($uc_order_products); + $entities = array_reverse($entities); // Node token replacements. if (!empty($nodes)) { @@ -556,7 +613,34 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = } } } - // Node token replacements. + + if (!empty($entities)) { + foreach ($entities as $entity_type => $entities_of_type) { + foreach ($entities_of_type as $entity) { + // We have to pass the correct data key. It needs to match the + // type the token expects. For now, we assume that the 'token type' + // matches what core and contributed entity type tokens expect. + // This is more accurate than using the entity type in cases such + // as taxonomy terms. They have an entity type of 'taxonomy_term', + // but they expect 'term' data. + if (!module_exists('entity_token')) { + // We can't provide good functionality without the Entity Tokens + // module. + break 2; + } + + $entity_info = entity_get_info($entity_type); + $token_type = !empty($entity_info['token type']) ? $entity_info['token type'] : $entity_type; + $entity_token_objects = array($token_type => $entity); + // @todo: array_merge() in loops is bad for performance. Would be good to rewrite this in a more performant way later if possible, especially once we start doing cleanups for Drupal 7 + PHP 7. + $token_objects = array_merge($token_objects, $entity_token_objects); + _fillpdf_merge_pdf_token_replace($obj->value, $token_objects, $token); + $transform_string = TRUE; + _fillpdf_process_image_tokens($entity_type, $entity, $obj, $fields, $image_data, $transform_string); + } + } + } + // Webform token replacements. if (!empty($webforms)) { foreach ($webforms as $webform) { @@ -602,7 +686,7 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = } } } - // Webform token replacements. + // Ubercart Order token replacements. if (!empty($uc_orders)) { foreach ($uc_orders as $uc_order) { @@ -613,7 +697,7 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = $transform_string = TRUE; } - // Ubercart Order token replacements. + // Ubercart Order Product token replacements. if (!empty($uc_order_products)) { foreach ($uc_order_products as $uc_order_product) { @@ -628,7 +712,7 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = $transform_string = TRUE; } - // Ubercart Order Product token replacements. + if ($transform_string) { // Replace <br /> occurrences with newlines. $str = preg_replace('|<br />|', ' @@ -675,7 +759,7 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = } // Assemble some metadata that will be useful for the handling phase. - $fillpdf_object = _fillpdf_build_options_object($force_download, $flatten, $fillpdf_info, $data, $nodes, $webforms, $uc_orders, $uc_order_products, $token_objects, $sample); + $fillpdf_object = _fillpdf_build_options_object($force_download, $flatten, $fillpdf_info, $data, $nodes, $webforms, $uc_orders, $uc_order_products, $token_objects, $sample, $entities); if ($handle === TRUE) { // Allow modules to step in here and change the way the PDF is handled. @@ -699,15 +783,17 @@ function fillpdf_merge_pdf($fid, $nids = NULL, $webform_array = NULL, $sample = * @param $uc_order_ids * @param $uc_order_product_ids * @param $user + * @param $entity_ids * * @return array */ -function fillpdf_load_entities($fillpdf_info, $nids, $webform_array, $uc_order_ids, $uc_order_product_ids, $user) { +function fillpdf_load_entities($fillpdf_info, $nids, $webform_array, $uc_order_ids, $uc_order_product_ids, $user, $entity_ids) { $context = array( 'nodes' => array(), 'webforms' => array(), 'uc_orders' => array(), 'uc_order_products' => array(), + 'entities' => array(), ); // If $webform_array contains entries with an sid, but not an nid, set the nid @@ -720,8 +806,11 @@ function fillpdf_load_entities($fillpdf_info, $nids, $webform_array, $uc_order_i } } - // If no nid is given, use the default. - if (!empty($fillpdf_info->default_nid) && empty($nids) && empty($webform_array)) { + $entity_mode = module_exists('entity_token'); + // If no nid is given, and Entity API is disabled, use the default nid as a + // classic node. (If Entity API is enabled, we'll handle it as an entity later + // on in this function.) + if (!$entity_mode && !empty($fillpdf_info->default_nid) && empty($nids) && empty($webform_array)) { $default_node = node_load($fillpdf_info->default_nid); if ($default_node) { // Default node is a non-webform node. @@ -790,6 +879,35 @@ function fillpdf_load_entities($fillpdf_info, $nids, $webform_array, $uc_order_i } return $context; } + + // Entities. + $entities_by_type = array(); + if ($entity_mode) { + // If no entity IDs are specified but we have a default NID, prime a plain + // entity ID here. The default entity type will be added just below. + if (empty($entity_ids) && !empty($fillpdf_info->default_nid)) { + $entity_ids = array($fillpdf_info->default_nid); + } + + if (!empty($entity_ids)) { + foreach ($entity_ids as $entity_id) { + list($type, $id) = strpos($entity_id, ':') ? explode(':', $entity_id) : array( + $entity_id, + NULL + ); + // Type might be missing, in which case we default to either the default + // entity type (if exists) or 'node' if none is set. + if (empty($id)) { + $id = $type; + $type = !empty($fillpdf_info->default_entity_type) ? $fillpdf_info->default_entity_type : 'node'; + } + $entities_by_type += array($type => array()); + $entities_by_type[$type][] = entity_load_single($type, $id); + } + $context['entities'] = $entities_by_type; + } + } + return $context; } @@ -809,7 +927,9 @@ function _fillpdf_process_image_tokens($entity_type, $entity, $obj, &$fields, &$ if (!$field_data['type'] === 'image') { continue; } - if ($obj->value === "[{$entity_type}:{$field_name}]") { + $info = entity_get_info($entity_type); + $token_type = !empty($info['token type']) ? $info['token type'] : $entity; + if ($obj->value === "[{$token_type}:{$field_name}]") { // It's a match! $image_field = field_get_items($entity_type, $entity, $field_name); if (!$image_field) { @@ -883,10 +1003,11 @@ function _fillpdf_merge_pdf_token_replace($value, $token_objects, &$existing_tok * @param $uc_orders * @param $uc_order_products * @param $token_objects - * + * @param bool $sample + * @param $entities * @return object */ -function _fillpdf_build_options_object($force_download, $flatten, $fillpdf_info, $data, $nodes, $webforms, $uc_orders, $uc_order_products, $token_objects, $sample = FALSE) { +function _fillpdf_build_options_object($force_download, $flatten, $fillpdf_info, $data, $nodes, $webforms, $uc_orders, $uc_order_products, $token_objects, $sample = FALSE, $entities) { // @todo: Convert function parameters to use $options // and add those into $fillpdf_info. $fillpdf_object = new stdClass(); @@ -897,6 +1018,7 @@ function _fillpdf_build_options_object($force_download, $flatten, $fillpdf_info, 'webforms' => $webforms, 'uc_orders' => $uc_orders, 'uc_order_products' => $uc_order_products, + 'entities' => $entities, ); $fillpdf_object->token_objects = $token_objects; $fillpdf_object->options = array( @@ -941,7 +1063,12 @@ function fillpdf_merge_perform_pdf_action($fillpdf_object, $action = 'download', if ($action == 'default') { // Determine the default action, then re-set $action to that. if (empty($pdf_info->destination_path) === FALSE) { - $action = 'save'; + if ($pdf_info->destination_redirect) { + $action = 'redirect'; + } + else { + $action = 'save'; + } } else { $action = 'download'; @@ -1076,8 +1203,14 @@ function fillpdf_fillpdf_merge_pre_handle($fillpdf) { * Make sure the user has access to data they want to populate the PDF. * * @todo: Support passing $account, for testability. + * @param array $nodes + * @param array $webforms + * @param array $uc_orders + * @param array $uc_order_products + * @param array $entities + * @return bool */ -function fillpdf_merge_pdf_access($nodes = array(), $webforms = array(), $uc_orders = array(), $uc_order_products = array()) { +function fillpdf_merge_pdf_access($nodes = array(), $webforms = array(), $uc_orders = array(), $uc_order_products = array(), $entities = array()) { if (user_access('administer pdfs') || user_access('publish all pdfs')) { return TRUE; } @@ -1090,6 +1223,7 @@ function fillpdf_merge_pdf_access($nodes = array(), $webforms = array(), $uc_ord if (empty($webforms)) { foreach ($nodes as $node) { // Own node? + // @todo: It's probably enough to check node_access() here. Figure out what the expected behavior should be, write a test, and remove the second condition if it isn't needed. Otherwise, add an appropriate comment. The permission is called "publish own pdfs", but it's really "publish pdfs" (without it or better, no PDFs can be published despite access). if (!(node_access('view', $node)) || ($node->uid != $user->uid)) { return FALSE; } @@ -1105,7 +1239,7 @@ function fillpdf_merge_pdf_access($nodes = array(), $webforms = array(), $uc_ord } // Own webform submission? - if (!(empty($webforms))) { + if (!empty($webforms)) { foreach ($webforms as $webform) { if (!(webform_submission_access($webform['webform'], $webform['submission'], 'view'))) { return FALSE; @@ -1140,6 +1274,17 @@ function fillpdf_merge_pdf_access($nodes = array(), $webforms = array(), $uc_ord } } + // Access to entities? + if (!empty($entities)) { + foreach ($entities as $entity_type => $entities_of_type) { + foreach ($entities_of_type as $entity_id => $entity) { + if (!entity_access('view', $entity_type, $entity, $account)) { + return FALSE; + } + } + } + } + // If no access checks have failed by this point, this must be a sample PDF, // and we allow it. return TRUE; diff --git a/tests/FillPdfMergeTestCase.test b/tests/FillPdfMergeTestCase.test index 6c88d43..bcb64ae 100644 --- a/tests/FillPdfMergeTestCase.test +++ b/tests/FillPdfMergeTestCase.test @@ -8,6 +8,8 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { use FillPdfTestHelper; protected $testNode; + protected $testEntity; + protected $nonPrivilegedUser; /** * @@ -29,7 +31,7 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { // ImageFieldTestCase::setUp() is dumb, so we have to enable some modules // ourselves. - $modules = array('fillpdf', 'fillpdf_test_webform', 'fillpdf_test'); + $modules = array('fillpdf', 'fillpdf_test_webform', 'entity_token', 'fillpdf_test'); $success = module_enable($modules, TRUE); $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules)))); @@ -44,10 +46,13 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { 'edit all webform submissions' => TRUE, 'edit webform components' => TRUE, )); + + $this->nonPrivilegedUser = $this->drupalCreateUser(array('publish own pdfs')); } /** * + * @throws \FieldException */ public function testPdfMerging() { features_revert_module('fillpdf_test_webform'); @@ -56,9 +61,7 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { $webform = webform_features_machine_name_load('fillpdf_test_webform'); $this->assertTrue(property_exists($webform, 'nid'), 'Webform properly loaded from Features.'); - // @todo: continue imitating D8 version. Create an image field (and content - // type, etc., if necessary). - $this->createImageField('field_fillpdf_test_image', 'article'); + $this->createImageField('field_test_image', 'article'); $files = $this->drupalGetTestFiles('image'); $image = reset($files); @@ -70,8 +73,7 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { 'create article content' => TRUE, )); - // @todo: Which user is actually logged in? - $this->testNode = node_load($this->uploadNodeImage($image, 'field_fillpdf_test_image', 'article')); + $this->testNode = node_load($this->uploadNodeImage($image, 'field_test_image', 'article')); // Test with a node. $this->uploadTestPdf(); @@ -101,8 +103,282 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { 'PDF is populated with the title of the node.' ); - $field_fillpdf_test_image = field_get_items('node', $this->testNode, 'field_fillpdf_test_image'); - $node_file = file_load($field_fillpdf_test_image[0]['fid']); + + /** + * These tests cover the official (and some unofficial) ways of building + * FillPDF Links for entities. Official: entity_id, entity_ids, entity_type + * + entity_id. Unofficial: entity (synonym for entity_id), entities + * (synonym for entity_ids). It doesn't test entity_type + entity, but that + * should work too. It's not officially supported, though. It's just that + * the contributed patch already had those working, and there was no good + * reason to take them out and make it harder for people to update their + * version of FillPDF later. + */ + $this->testEntity = node_load($this->uploadNodeImage($image, 'field_test_image', 'article')); + + // Test with a node. + $this->uploadTestPdf(); + $fillpdf_form_entity = fillpdf_load($this->getLatestFillPdfForm()); + + // Get the field definitions for the form that was created and configure + // them. + $entity_fid = $fillpdf_form_entity->fid; + $entity_fields = fillpdf_get_fields($entity_fid); + $this->mapFillPdfFieldsToNodeFields($entity_fields, $entity_fid); + + // Hit the FillPDF URL, check the results from the test fill method. + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_fid, + 'entity_id' => $this->testEntity->nid, + ), + )); + + // We don't actually care about downloading the fake PDF. We just want to + // check what happened in the backend. + $entity_merge_result_1 = variable_get('fillpdf_test_last_merge_metadata'); + + $this->assertEqual( + $entity_merge_result_1['fields']['TextField'], + $this->testEntity->title, + 'PDF is populated with the title of the node (via entity tokens + and single-entity format).' + ); + + + // Hit the FillPDF URL, check the results from the test fill method. + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_fid, + 'entity' => "node:{$this->testEntity->nid}", + ), + )); + + // We don't actually care about downloading the fake PDF. We just want to + // check what happened in the backend. + $entity_merge_result_2 = variable_get('fillpdf_test_last_merge_metadata'); + + $this->assertEqual( + $entity_merge_result_2['fields']['TextField'], + $this->testEntity->title, + 'PDF is populated with the title of the node (via entity tokens + and alternative single-entity format).' + ); + + $this->createImageFieldForTaxonomyTerm('field_term_image', 'tags'); + $term_image = file_save($image); + + // Test an actual entity token (taxonomy terms). + $test_term = entity_create('taxonomy_term', array( + 'name' => 'Test term', + 'vid' => taxonomy_vocabulary_machine_name_load('tags')->vid, + 'field_term_image' => array(LANGUAGE_NONE => array(0 => (array) $term_image)), + )); + taxonomy_term_save($test_term); + + // Re-map the PDF fields so that the following tests will work. + $this->mapFillPdfFieldsToTaxonomyTermFields($entity_fields, $entity_fid); + + // Hit the FillPDF URL, check the results from the test fill method. + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_fid, + 'entity_ids' => array("taxonomy_term:{$test_term->tid}"), + ), + )); + + // We don't actually care about downloading the fake PDF. We just want to + // check what happened in the backend. + $entity_merge_result_3 = variable_get('fillpdf_test_last_merge_metadata'); + + $this->assertEqual( + $entity_merge_result_3['fields']['TextField'], + $test_term->name, + 'PDF is populated with the title of the taxonomy term.' + ); + + $field_term_image = field_get_items('taxonomy_term', $test_term, 'field_term_image'); + $term_file = file_load($field_term_image[0]['fid']); + $this->assertEqual( + $entity_merge_result_3['images']['ImageField']['data'], + base64_encode(file_get_contents($term_file->uri)), + '(Entity mode) Encoded image matches known image.' + ); + + $path_info = pathinfo($term_file->uri); + $expected_file_hash = md5($path_info['filename']) . '.' . $path_info['extension']; + $this->assertEqual( + $entity_merge_result_3['images']['ImageField']['filenamehash'], + $expected_file_hash, + '(Entity mode) Hashed filename matches known hash.' + ); + + $this->assertEqual( + $entity_merge_result_3['fields']['ImageField'], + '{image}' . drupal_realpath($term_file->uri), + '(Entity mode) URI in metadata matches expected URI.' + ); + + // Clean up, since we're reusing these variables again later in the same + // test. They weren't worth renaming. + unset($path_info, $expected_file_hash); + + + // Hit the FillPDF URL, check the results from the test fill method. + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_fid, + 'entities' => array("taxonomy_term:{$test_term->tid}"), + ), + )); + + // We don't actually care about downloading the fake PDF. We just want to + // check what happened in the backend. + $entity_merge_result_4 = variable_get('fillpdf_test_last_merge_metadata'); + + $this->assertEqual( + $entity_merge_result_4['fields']['TextField'], + $test_term->name, + 'PDF is populated with the title of the taxonomy term.' + ); + + + // Hit the FillPDF URL, check the results from the test fill method. + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_fid, + 'entity_type' => 'taxonomy_term', + 'entity_id' => $test_term->tid, + ), + )); + + // We don't actually care about downloading the fake PDF. We just want to + // check what happened in the backend. + $entity_merge_result_5 = variable_get('fillpdf_test_last_merge_metadata'); + + $this->assertEqual( + $entity_merge_result_5['fields']['TextField'], + $test_term->name, + 'PDF is populated with the title of the taxonomy term (via + simplified format).' + ); + + + // Test filename title generation. Configure it, merge the PDF without + // handling, and then save it as a file and check the name. + db_update('fillpdf_forms') + ->fields(array( + 'destination_path' => 'output', + 'title' => 'Token_[term:name]_title', + 'destination_redirect' => 1, + )) + ->condition('fid', $entity_fid) + ->execute(); + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_fid, + 'entity' => "taxonomy_term:{$test_term->tid}", + ), + )); + $where_are_we = $this->getUrl(); + $path_parts = explode('/', parse_url($where_are_we, PHP_URL_PATH)); + $filename = end($path_parts); + if (empty($filename)) { + // Wait, are we the d.o. testbot? Who else turns off clean URLs? + // Alright, then; try the query string instead. + $raw_query = parse_url($where_are_we, PHP_URL_QUERY); + parse_str($raw_query, $parse_query); + if (isset($parse_query['q'])) { + $query_parts = explode('/', $parse_query['q']); + $filename = end($query_parts); + } + } + $expected_filename = 'Token_Test_term_title.pdf'; + $this->assertEqual($expected_filename, $filename, t('Filename of generated file (@actual) matches specified entity token pattern (@expected). Current page URL: @url', array('@actual' => $filename, '@expected' => $expected_filename, '@url' => $where_are_we))); + + + // Test that node access via entity access works. + // Make a basic page. + $entity_access_test_node = new stdClass(); + $entity_access_test_node->type = 'page'; + $entity_access_test_node->title = t('Entity access test'); + $entity_access_test_node->field_body = array( + LANGUAGE_NONE => array( + 0 => array( + 'value' => 'This is test text.', + ), + ), + ); + $entity_access_test_node->uid = 1; + node_save($entity_access_test_node); + + // Upload a fresh PDF, since we changed some settings on the other one. + $this->uploadTestPdf(); + $entity_access_pdf = fillpdf_load($this->getLatestFillPdfForm()); + + // Get the field definitions for the form that was created and configure + // them. + $entity_access_fid = $entity_access_pdf->fid; + $entity_access_fields = fillpdf_get_fields($entity_access_fid); + $this->mapFillPdfFieldsToNodeFields($entity_access_fields, $entity_access_fid); + + $this->drupalLogin($this->nonPrivilegedUser); + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_access_fid, + 'entity' => "node:{$entity_access_test_node->nid}", + ), + )); + + $this->assertResponse(403, 'User must have access to base entity to fill in PDF.'); + + // Restore privileged user. + $this->drupalLogin($this->privilegedUser); + + // Test classic default NID handling (turn off the Entity API module). + // It's enough to disable entity_token without the parent entity module, + // since we always check for entity_token specifically. + module_disable(array('entity_token')); + $this->drupalPost("admin/structure/fillpdf/{$entity_access_fid}", array( + 'default_nid' => $entity_access_test_node->nid, + ), t('Update')); + // Ensure default_entity_type is NULL in the database, as this is a + // regression to make sure old-style default_nid isn't broken. + db_update('fillpdf_forms') + ->condition('fid', $entity_access_fid) + ->fields(array('default_entity_type' => NULL)) + ->execute(); + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_access_fid, + ), + )); + + $default_nid_merge_result = variable_get('fillpdf_test_last_merge_metadata'); + $this->assertEqual('Entity access test', $default_nid_merge_result['fields']['TextField'], 'Configured node properly used as default when no parameters specified.'); + + + // Re-enable Entity Tokens. + module_enable(array('entity_token')); + + // Test default entity type/entity ID. We use the browser to set these to + // ensure the fields are there. + $this->drupalPost("admin/structure/fillpdf/{$entity_fid}", array( + 'default_nid' => $test_term->tid, + 'default_entity_type' => 'taxonomy_term', + ), t('Update')); + $this->drupalGet('fillpdf', array( + 'query' => array( + 'fid' => $entity_fid, + ), + )); + + $default_entity_merge_result = variable_get('fillpdf_test_last_merge_metadata'); + $this->assertEqual('Test term', $default_entity_merge_result['fields']['TextField'], 'Test term set as default entity properly used when no parameters specified.'); + + + $field_test_image = field_get_items('node', $this->testNode, 'field_test_image'); + $node_file = file_load($field_test_image[0]['fid']); $this->assertEqual( $merge_result['images']['ImageField']['data'], base64_encode(file_get_contents($node_file->uri)), @@ -128,7 +404,7 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { foreach ($legacy_fields as $legacy_pdf_key => $legacy_field) { switch ($legacy_pdf_key) { case 'ImageField': - $legacy_field['value'] = '[stamp:field_fillpdf_test_image]'; + $legacy_field['value'] = '[stamp:field_test_image]'; break; } fillpdf_fields_create_update($fid, $legacy_pdf_key, $legacy_field, TRUE); @@ -298,7 +574,7 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { foreach ($fields as $pdf_key => $field) { switch ($pdf_key) { case 'ImageField': - $field['value'] = '[node:field_fillpdf_test_image]'; + $field['value'] = '[node:field_test_image]'; break; case 'TextField': @@ -309,4 +585,23 @@ class FillPdfMergeTestCase extends ImageFieldTestCase { } } + /** + * @param $fields + * @param $fid + */ + protected function mapFillPdfFieldsToTaxonomyTermFields($fields, $fid) { + foreach ($fields as $pdf_key => $field) { + switch ($pdf_key) { + case 'ImageField': + $field['value'] = '[term:field_term_image]'; + break; + + case 'TextField': + $field['value'] = '[term:name]'; + break; + } + fillpdf_fields_create_update($fid, $pdf_key, $field, TRUE); + } + } + } diff --git a/tests/FillPdfTestCase.test b/tests/FillPdfTestCase.test index 7958168..22fb6bc 100644 --- a/tests/FillPdfTestCase.test +++ b/tests/FillPdfTestCase.test @@ -32,11 +32,11 @@ class FillPdfTestCase extends FileFieldTestCase { public function setUp() { // Enable any modules required for the test. This should be an array of // module names. - parent::setUp(array('fillpdf', 'fillpdf_test')); + parent::setUp(array('entity_token', 'fillpdf', 'fillpdf_test')); $this->createPrivilegedUser(); - $this->nonPrivilegedUser = $this->drupalCreateUser(); + $this->nonPrivilegedUser = $this->drupalCreateUser(array('publish own pdfs')); } /** @@ -48,6 +48,7 @@ class FillPdfTestCase extends FileFieldTestCase { /** * Ensure that fillpdf_link_from_context() functions properly. + * @throws \EntityMalformedException */ public function testLinkFromContext() { // @todo: flesh out with more combinations. This one was most broken, @@ -104,6 +105,21 @@ class FillPdfTestCase extends FileFieldTestCase { $actual_link2 = fillpdf_context_to_link($fid, $fake_multiple_context); $this->assertEqual($expected_link2, $actual_link2, 'fillpdf_context_to_link() generates a link with multiple Webforms correctly.'); + + $test_node = entity_create('node', array( + 'type' => 'article', + )); + $test_node->nid = 123; + $fake_entity_context = array( + 'entities' => array( + 'node' => array($test_node), + ), + ); + + $expected_link3 = url('', array('absolute' => TRUE)) . 'fillpdf?fid=1&entity_ids[]=node:123'; + $actual_link3 = fillpdf_context_to_link($fid, $fake_entity_context); + + $this->assertEqual($expected_link3, $actual_link3, 'fillpdf_context_to_link() generates a link with entities correctly.'); } /** @@ -157,6 +173,26 @@ class FillPdfTestCase extends FileFieldTestCase { $this->drupalLogin($this->nonPrivilegedUser); $this->drupalGet('system/files/fillpdf/output/fillpdf_test_v4.pdf'); $this->assertResponse(403, 'User without Administer PDFs and without Publish All PDFs cannot access PDF they cannot view the node for.'); + + + // Test access when generated through entities. + $this->drupalLogin($this->privilegedUser); + $fillpdf_object = fillpdf_merge_pdf($fid, NULL, NULL, NULL, FALSE, FALSE, TRUE, FALSE, NULL, NULL, array("node:{$new_node->nid}")); + $saved_file_2 = fillpdf_action_save_to_file($fillpdf_object, 'fillpdf_test_entity_v4.pdf', FALSE, FALSE); + $saved_file_2->display = 1; + + $new_node->field_pdf = array( + LANGUAGE_NONE => array( + 0 => (array) $saved_file_2, + ), + ); + node_save($new_node); + $this->drupalGet('system/files/fillpdf/output/fillpdf_test_entity_v4.pdf'); + $this->assertResponse(200, 'Entity mode: User can generate and access PDF from any data when they have the Publish All PDFs permission.'); + + $this->drupalLogin($this->nonPrivilegedUser); + $this->drupalGet('system/files/fillpdf/output/fillpdf_test_entity_v4.pdf'); + $this->assertResponse(403, 'Entity mode: User without Administer PDFs and without Publish All PDFs cannot access PDF they cannot view the node for.'); } } diff --git a/tests/FillPdfTestHelper.test b/tests/FillPdfTestHelper.test index 00d4d49..49a54b3 100644 --- a/tests/FillPdfTestHelper.test +++ b/tests/FillPdfTestHelper.test @@ -70,4 +70,48 @@ trait FillPdfTestHelper { ->fetchField(); } + /** + * Create a new image field against a taxonomy term type. + * @see \ImageFieldTestCase::createImageField() + * + * @param $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param $type_name + * The node type that this field will be added to. + * @param $field_settings + * A list of field settings that will be added to the defaults. + * @param $instance_settings + * A list of instance settings that will be added to the instance defaults. + * @param $widget_settings + * A list of widget settings that will be added to the widget defaults. + * @return mixed + * @throws \FieldException + */ + protected function createImageFieldForTaxonomyTerm($name, $type_name, $field_settings = array(), $instance_settings = array(), $widget_settings = array()) { + $field = array( + 'field_name' => $name, + 'type' => 'image', + 'settings' => array(), + 'cardinality' => !empty($field_settings['cardinality']) ? $field_settings['cardinality'] : 1, + ); + $field['settings'] = array_merge($field['settings'], $field_settings); + field_create_field($field); + + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => 'taxonomy_term', + 'label' => $name, + 'bundle' => $type_name, + 'required' => !empty($instance_settings['required']), + 'settings' => array(), + 'widget' => array( + 'type' => 'image_image', + 'settings' => array(), + ), + ); + $instance['settings'] = array_merge($instance['settings'], $instance_settings); + $instance['widget']['settings'] = array_merge($instance['widget']['settings'], $widget_settings); + return field_create_instance($instance); + } + } diff --git a/tests/modules/fillpdf_test/fillpdf_test.info b/tests/modules/fillpdf_test/fillpdf_test.info index 892acdd..d2bf245 100644 --- a/tests/modules/fillpdf_test/fillpdf_test.info +++ b/tests/modules/fillpdf_test/fillpdf_test.info @@ -5,5 +5,7 @@ package = Testing dependencies[] = fillpdf dependencies[] = fillpdf_test_webform +test_dependencies[] = entity_token + ; This is a test module. hidden = TRUE -- GitLab