feeds_ui.admin.inc 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  1. <?php
  2. /**
  3. * @file
  4. * Contains all page callbacks, forms and theming functions for Feeds
  5. * administrative pages.
  6. */
  7. /**
  8. * Introductory help for admin/structure/feeds/%feeds_importer page
  9. */
  10. function feeds_ui_edit_help() {
  11. return t('
  12. <p>
  13. You can create as many Feeds importer configurations as you would like to. Each can have a distinct purpose like letting your users aggregate RSS feeds or importing a CSV file for content migration. Here are a couple of things that are important to understand in order to get started with Feeds:
  14. </p>
  15. <ul>
  16. <li>
  17. Every importer configuration consists of basic settings, a fetcher, a parser and a processor and their settings.
  18. </li>
  19. <li>
  20. The <strong>basic settings</strong> define the general behavior of the importer. <strong>Fetchers</strong> are responsible for loading data, <strong>parsers</strong> for organizing it and <strong>processors</strong> for "doing stuff" with it, usually storing it.
  21. </li>
  22. <li>
  23. In Basic settings, you can <strong>attach an importer configuration to a content type</strong>. This is useful when many imports of a kind should be created, for example in an RSS aggregation scenario. If you don\'t attach a configuration to a content type, you can use it on the !import page.
  24. </li>
  25. <li>
  26. Imports can be <strong>scheduled periodically</strong> - see the periodic import select box in the Basic settings.
  27. </li>
  28. <li>
  29. Processors can have <strong>mappings</strong> in addition to settings. Mappings allow you to define what elements of a data feed should be mapped to what content fields on a granular level. For instance, you can specify that a feed item\'s author should be mapped to a node\'s body.
  30. </li>
  31. </ul>
  32. ', array('!import' => l(t('Import'), 'import')));
  33. }
  34. /**
  35. * Help text for mapping.
  36. */
  37. function feeds_ui_mapping_help() {
  38. return t('
  39. <p>
  40. Define which elements of a single item of a feed (= <em>Sources</em>) map to which content pieces in Drupal (= <em>Targets</em>). Make sure that at least one definition has a <em>Unique target</em>. A unique target means that a value for a target can only occur once. E. g. only one item with the URL <em>http://example.com/content/1</em> can exist.
  41. </p>
  42. ');
  43. }
  44. /**
  45. * Build overview of available configurations.
  46. */
  47. function feeds_ui_overview_form($form, &$form_status) {
  48. $form = $form['enabled'] = $form['disabled'] = array();
  49. $form['#header'] = array(
  50. t('Name'),
  51. t('Description'),
  52. t('Attached to'),
  53. t('Status'),
  54. t('Operations'),
  55. t('Enabled'),
  56. );
  57. foreach (feeds_importer_load_all(TRUE) as $importer) {
  58. $importer_form = array();
  59. $importer_form['name']['#markup'] = check_plain($importer->config['name']);
  60. $importer_form['description']['#markup'] = check_plain($importer->config['description']);
  61. if (empty($importer->config['content_type'])) {
  62. $importer_form['attached']['#markup'] = '[none]';
  63. }
  64. else {
  65. if (!$importer->disabled) {
  66. $importer_form['attached']['#markup'] = l(node_type_get_name($importer->config['content_type']), 'node/add/' . str_replace('_', '-', $importer->config['content_type']));
  67. }
  68. else {
  69. $importer_form['attached']['#markup'] = check_plain(node_type_get_name($importer->config['content_type']));
  70. }
  71. }
  72. if ($importer->export_type == EXPORT_IN_CODE) {
  73. $status = t('Default');
  74. $edit = t('Override');
  75. $delete = '';
  76. }
  77. elseif ($importer->export_type == EXPORT_IN_DATABASE) {
  78. $status = t('Normal');
  79. $edit = t('Edit');
  80. $delete = t('Delete');
  81. }
  82. elseif ($importer->export_type == (EXPORT_IN_CODE | EXPORT_IN_DATABASE)) {
  83. $status = t('Overridden');
  84. $edit = t('Edit');
  85. $delete = t('Revert');
  86. }
  87. $importer_form['status'] = array(
  88. '#markup' => $status,
  89. );
  90. $importer_form['operations'] = array(
  91. '#markup' =>
  92. l($edit, 'admin/structure/feeds/' . $importer->id) . ' | ' .
  93. l(t('Export'), 'admin/structure/feeds/' . $importer->id . '/export') . ' | ' .
  94. l(t('Clone'), 'admin/structure/feeds/' . $importer->id . '/clone') .
  95. (empty($delete) ? '' : ' | ' . l($delete, 'admin/structure/feeds/' . $importer->id . '/delete')),
  96. );
  97. $importer_form[$importer->id] = array(
  98. '#type' => 'checkbox',
  99. '#default_value' => !$importer->disabled,
  100. );
  101. if ($importer->disabled) {
  102. $form['disabled'][$importer->id] = $importer_form;
  103. }
  104. else {
  105. $form['enabled'][$importer->id] = $importer_form;
  106. }
  107. }
  108. $form['submit'] = array(
  109. '#type' => 'submit',
  110. '#value' => t('Save'),
  111. );
  112. return $form;
  113. }
  114. /**
  115. * Submit handler for feeds_ui_overview_form().
  116. */
  117. function feeds_ui_overview_form_submit($form, &$form_state) {
  118. $disabled = array();
  119. foreach (feeds_importer_load_all(TRUE) as $importer) {
  120. $disabled[$importer->id] = !$form_state['values'][$importer->id];
  121. }
  122. variable_set('default_feeds_importer', $disabled);
  123. feeds_cache_clear();
  124. }
  125. /**
  126. * Create a new configuration.
  127. *
  128. * @param $form_state
  129. * Form API form state array.
  130. * @param $from_importer
  131. * FeedsImporter object. If given, form will create a new importer as a copy
  132. * of $from_importer.
  133. */
  134. function feeds_ui_create_form($form, &$form_state, $from_importer = NULL) {
  135. $form['#from_importer'] = $from_importer;
  136. $form['name'] = array(
  137. '#type' => 'textfield',
  138. '#title' => t('Name'),
  139. '#description' => t('A natural name for this configuration. Example: RSS Feed. You can always change this name later.'),
  140. '#required' => TRUE,
  141. '#maxlength' => 128,
  142. );
  143. $form['id'] = array(
  144. '#type' => 'machine_name',
  145. '#required' => TRUE,
  146. '#maxlength' => 128,
  147. '#machine_name' => array(
  148. 'exists' => 'feeds_ui_importer_machine_name_exists',
  149. ),
  150. );
  151. $form['description'] = array(
  152. '#type' => 'textfield',
  153. '#title' => t('Description'),
  154. '#description' => t('A description of this configuration.'),
  155. );
  156. $form['submit'] = array(
  157. '#type' => 'submit',
  158. '#value' => t('Create'),
  159. );
  160. return $form;
  161. }
  162. /**
  163. * Validation callback for the importer machine name field.
  164. */
  165. function feeds_ui_importer_machine_name_exists($id) {
  166. if ($id == 'create') {
  167. // Create is a reserved path for the add importer form.
  168. return TRUE;
  169. }
  170. ctools_include('export');
  171. if (ctools_export_load_object('feeds_importer', 'conditions', array('id' => $id))) {
  172. return TRUE;
  173. }
  174. }
  175. /**
  176. * Validation handler for feeds_build_create_form().
  177. */
  178. function feeds_ui_create_form_validate($form, &$form_state) {
  179. if (!empty($form_state['values']['id'])) {
  180. $importer = feeds_importer($form_state['values']['id']);
  181. $importer->configFormValidate($form_state['values']);
  182. }
  183. }
  184. /**
  185. * Submit handler for feeds_build_create_form().
  186. */
  187. function feeds_ui_create_form_submit($form, &$form_state) {
  188. // Create feed.
  189. $importer = feeds_importer($form_state['values']['id']);
  190. // If from_importer is given, copy its configuration.
  191. if (!empty($form['#from_importer'])) {
  192. $importer->copy($form['#from_importer']);
  193. }
  194. // In any case, we want to set this configuration's title and description.
  195. $importer->addConfig($form_state['values']);
  196. $importer->save();
  197. // Set a message and redirect to settings form.
  198. if (empty($form['#from_importer'])) {
  199. drupal_set_message(t('Your configuration has been created with default settings. If they do not fit your use case you can adjust them here.'));
  200. }
  201. else {
  202. drupal_set_message(t('A clone of the @name configuration has been created.', array('@name' => $form['#from_importer']->config['name'])));
  203. }
  204. $form_state['redirect'] = 'admin/structure/feeds/' . $importer->id;
  205. feeds_cache_clear();
  206. }
  207. /**
  208. * Delete configuration form.
  209. */
  210. function feeds_ui_delete_form($form, &$form_state, $importer) {
  211. $form['#importer'] = $importer->id;
  212. if ($importer->export_type & EXPORT_IN_CODE) {
  213. $title = t('Would you really like to revert the importer @importer?', array('@importer' => $importer->config['name']));
  214. $button_label = t('Revert');
  215. }
  216. else {
  217. $title = t('Would you really like to delete the importer @importer?', array('@importer' => $importer->config['name']));
  218. $button_label = t('Delete');
  219. }
  220. return confirm_form(
  221. $form,
  222. $title,
  223. 'admin/structure/feeds',
  224. t('This action cannot be undone.'),
  225. $button_label
  226. );
  227. }
  228. /**
  229. * Submit handler for feeds_ui_delete_form().
  230. */
  231. function feeds_ui_delete_form_submit($form, &$form_state) {
  232. $form_state['redirect'] = 'admin/structure/feeds';
  233. // Remove importer.
  234. feeds_importer($form['#importer'])->delete();
  235. // Clear cache, deleting a configuration may have an affect on menu tree.
  236. feeds_cache_clear();
  237. }
  238. /**
  239. * Export a feed configuration.
  240. */
  241. function feeds_ui_export_form($form, &$form_state, $importer) {
  242. $code = feeds_export($importer->id);
  243. $form['export'] = array(
  244. '#title' => t('Export feed configuration'),
  245. '#type' => 'textarea',
  246. '#value' => $code,
  247. '#rows' => substr_count($code, "\n"),
  248. );
  249. return $form;
  250. }
  251. /**
  252. * Edit feed configuration.
  253. */
  254. function feeds_ui_edit_page(FeedsImporter $importer, $active = 'help', $plugin_key = '') {
  255. // Get plugins and configuration.
  256. $plugins = FeedsPlugin::all();
  257. $config = $importer->config;
  258. // Base path for changing the active container.
  259. $path = 'admin/structure/feeds/' . $importer->id;
  260. $active_container = array(
  261. 'class' => array('active-container'),
  262. 'actions' => array(l(t('Help'), $path)),
  263. );
  264. switch ($active) {
  265. case 'help':
  266. $active_container['title'] = t('Getting started');
  267. $active_container['body'] = '<div class="help feeds-admin-ui">' . feeds_ui_edit_help() . '</div>';
  268. unset($active_container['actions']);
  269. break;
  270. case 'fetcher':
  271. case 'parser':
  272. case 'processor':
  273. $active_container['title'] = t('Select a !plugin_type', array('!plugin_type' => $active));
  274. $active_container['body'] = drupal_get_form('feeds_ui_plugin_form', $importer, $active);
  275. break;
  276. case 'settings':
  277. drupal_add_js(drupal_get_path('module', 'ctools') . '/js/dependent.js');
  278. ctools_include('dependent');
  279. if (empty($plugin_key)) {
  280. $active_container['title'] = t('Basic settings');
  281. $active_container['body'] = feeds_get_form($importer, 'configForm');
  282. }
  283. // feeds_plugin() returns a correct result because feed has been
  284. // instantiated previously.
  285. elseif (in_array($plugin_key, array_keys($plugins)) && $plugin = feeds_plugin($plugin_key, $importer->id)) {
  286. $active_container['title'] = t('Settings for !plugin', array('!plugin' => $plugins[$plugin_key]['name']));
  287. $active_container['body'] = feeds_get_form($plugin, 'configForm');
  288. }
  289. break;
  290. case 'mapping':
  291. $processor_name = isset($plugins[$config['processor']['plugin_key']]['name']) ? $plugins[$config['processor']['plugin_key']]['name'] : $plugins['FeedsMissingPlugin']['name'];
  292. $active_container['title'] = t('Mapping for @processor', array('@processor' => $processor_name));
  293. $active_container['body'] = drupal_get_form('feeds_ui_mapping_form', $importer);
  294. break;
  295. }
  296. // Build config info.
  297. $config_info = $info = array();
  298. $info['class'] = array('config-set');
  299. // Basic information.
  300. $items = array();
  301. $items[] = t('Attached to: @type', array('@type' => $importer->config['content_type'] ? node_type_get_name($importer->config['content_type']) : t('[none]')));
  302. if ($importer->config['import_period'] == FEEDS_SCHEDULE_NEVER) {
  303. $import_period = t('off');
  304. }
  305. elseif ($importer->config['import_period'] == 0) {
  306. $import_period = t('as often as possible');
  307. }
  308. else {
  309. $import_period = t('every !interval', array('!interval' => format_interval($importer->config['import_period'])));
  310. }
  311. $items[] = t('Periodic import: !import_period', array('!import_period' => $import_period));
  312. $items[] = $importer->config['import_on_create'] ? t('Import on submission') : t('Do not import on submission');
  313. $info['title'] = t('Basic settings');
  314. $info['body'] = array(
  315. array(
  316. 'body' => theme('item_list', array('items' => $items)),
  317. 'actions' => array(l(t('Settings'), $path . '/settings')),
  318. ),
  319. );
  320. $config_info[] = $info;
  321. // Fetcher.
  322. $fetcher = isset($plugins[$config['fetcher']['plugin_key']]) ? $plugins[$config['fetcher']['plugin_key']] : $plugins['FeedsMissingPlugin'];
  323. $actions = array();
  324. if ($importer->fetcher->hasConfigForm()) {
  325. $actions = array(l(t('Settings'), $path . '/settings/' . $config['fetcher']['plugin_key']));
  326. }
  327. $info['title'] = t('Fetcher');
  328. $info['body'] = array(
  329. array(
  330. 'title' => $fetcher['name'],
  331. 'body' => $fetcher['description'],
  332. 'actions' => $actions,
  333. ),
  334. );
  335. $info['actions'] = array(l(t('Change'), $path . '/fetcher'));
  336. $config_info[] = $info;
  337. // Parser.
  338. $parser = isset($plugins[$config['parser']['plugin_key']]) ? $plugins[$config['parser']['plugin_key']] : $plugins['FeedsMissingPlugin'];
  339. $actions = array();
  340. if ($importer->parser->hasConfigForm()) {
  341. $actions = array(l(t('Settings'), $path . '/settings/' . $config['parser']['plugin_key']));
  342. }
  343. $info['title'] = t('Parser');
  344. $info['body'] = array(
  345. array(
  346. 'title' => $parser['name'],
  347. 'body' => $parser['description'],
  348. 'actions' => $actions,
  349. )
  350. );
  351. $info['actions'] = array(l(t('Change'), $path . '/parser'));
  352. $config_info[] = $info;
  353. // Processor.
  354. $processor = isset($plugins[$config['processor']['plugin_key']]) ? $plugins[$config['processor']['plugin_key']] : $plugins['FeedsMissingPlugin'];
  355. $actions = array();
  356. if ($importer->processor->hasConfigForm()) {
  357. $actions[] = l(t('Settings'), $path . '/settings/' . $config['processor']['plugin_key']);
  358. }
  359. $actions[] = l(t('Mapping'), $path . '/mapping');
  360. $info['title'] = t('Processor');
  361. $info['body'] = array(
  362. array(
  363. 'title' => $processor['name'],
  364. 'body' => $processor['description'],
  365. 'actions' => $actions,
  366. )
  367. );
  368. $info['actions'] = array(l(t('Change'), $path . '/processor'));
  369. $config_info[] = $info;
  370. return theme('feeds_ui_edit_page', array(
  371. 'info' => $config_info,
  372. 'active' => $active_container,
  373. ));
  374. }
  375. /**
  376. * Build a form of plugins to pick from.
  377. *
  378. * @param $form_state
  379. * Form API form state array.
  380. * @param $importer
  381. * FeedsImporter object.
  382. * @param $type
  383. * Plugin type. One of 'fetcher', 'parser', 'processor'.
  384. *
  385. * @return
  386. * A Form API form definition.
  387. */
  388. function feeds_ui_plugin_form($form, &$form_state, $importer, $type) {
  389. $plugins = FeedsPlugin::byType($type);
  390. $form['#importer'] = $importer->id;
  391. $form['#plugin_type'] = $type;
  392. $importer_key = $importer->config[$type]['plugin_key'];
  393. foreach ($plugins as $key => $plugin) {
  394. $form['plugin_key'][$key] = array(
  395. '#type' => 'radio',
  396. '#parents' => array('plugin_key'),
  397. '#title' => check_plain($plugin['name']),
  398. '#description' => filter_xss(isset($plugin['help']) ? $plugin['help'] : $plugin['description']),
  399. '#return_value' => $key,
  400. '#default_value' => ($key == $importer_key) ? $key : '',
  401. );
  402. }
  403. $form['submit'] = array(
  404. '#type' => 'submit',
  405. '#value' => t('Save'),
  406. );
  407. return $form;
  408. }
  409. /**
  410. * Submit handler for feeds_ui_plugin_form().
  411. */
  412. function feeds_ui_plugin_form_submit($form, &$form_state) {
  413. // Set the plugin and save feed.
  414. $importer = feeds_importer($form['#importer']);
  415. $importer->setPlugin($form_state['values']['plugin_key']);
  416. $importer->save();
  417. drupal_set_message(t('Changed @type plugin.', array('@type' => $form['#plugin_type'])));
  418. }
  419. /**
  420. * Theme feeds_ui_plugin_form().
  421. */
  422. function theme_feeds_ui_plugin_form($variables) {
  423. $form = $variables['form'];
  424. $output = '';
  425. foreach (element_children($form['plugin_key']) as $key) {
  426. // Assemble container, render form elements.
  427. $container = array(
  428. 'title' => $form['plugin_key'][$key]['#title'],
  429. 'body' => isset($form['plugin_key'][$key]['#description']) ? $form['plugin_key'][$key]['#description'] : '',
  430. );
  431. $form['plugin_key'][$key]['#title'] = t('Select');
  432. $form['plugin_key'][$key]['#attributes']['class'] = array('feeds-ui-radio-link');
  433. unset($form['plugin_key'][$key]['#description']);
  434. $container['actions'] = array(drupal_render($form['plugin_key'][$key]));
  435. $output .= theme('feeds_ui_container', array('container' => $container));
  436. }
  437. $output .= drupal_render_children($form);
  438. return $output;
  439. }
  440. /**
  441. * Edit mapping.
  442. *
  443. * @todo Completely merge this into config form handling. This is just a
  444. * shared form of configuration, most of the common functionality can live in
  445. * FeedsProcessor, a flag can tell whether mapping is supported or not.
  446. */
  447. function feeds_ui_mapping_form($form, &$form_state, $importer) {
  448. $form['#importer'] = $importer->id;
  449. $form['#mappings'] = $mappings = $importer->processor->getMappings();
  450. $form['help']['#markup'] = feeds_ui_mapping_help();
  451. $form['#prefix'] = '<div id="feeds-ui-mapping-form-wrapper">';
  452. $form['#suffix'] = '</div>';
  453. // Show message when target configuration gets changed.
  454. if (!empty($form_state['mapping_settings'])) {
  455. $form['#mapping_settings'] = $form_state['mapping_settings'];
  456. $form['changed'] = array(
  457. '#theme_wrappers' => array('container'),
  458. '#attributes' => array('class' => array('messages', 'warning')),
  459. '#markup' => t('* Changes made to target configuration are stored temporarily. Click Save to make your changes permanent.'),
  460. );
  461. }
  462. // Get mapping sources from parsers and targets from processor, format them
  463. // for output.
  464. // Some parsers do not define mapping sources but let them define on the fly.
  465. if ($sources = $importer->parser->getMappingSources()) {
  466. $source_options = _feeds_ui_format_options($sources);
  467. foreach ($sources as $k => $source) {
  468. if (!empty($source['deprecated'])) {
  469. continue;
  470. }
  471. $legend['sources'][$k]['name']['#markup'] = empty($source['name']) ? $k : $source['name'];
  472. $legend['sources'][$k]['description']['#markup'] = empty($source['description']) ? '' : $source['description'];
  473. }
  474. }
  475. else {
  476. $legend['sources']['#markup'] = t('This parser supports free source definitions. Enter the name of the source field in lower case into the Source text field above.');
  477. }
  478. $targets = $importer->processor->getMappingTargets();
  479. $target_options = _feeds_ui_format_options($targets);
  480. $legend['targets'] = array();
  481. foreach ($targets as $k => $target) {
  482. if (!empty($target['deprecated'])) {
  483. continue;
  484. }
  485. $legend['targets'][$k]['name']['#markup'] = empty($target['name']) ? $k : $target['name'];
  486. $legend['targets'][$k]['description']['#markup'] = empty($target['description']) ? '' : $target['description'];
  487. }
  488. // Legend explaining source and target elements.
  489. $form['legendset'] = array(
  490. '#type' => 'fieldset',
  491. '#title' => t('Legend'),
  492. '#collapsible' => TRUE,
  493. '#collapsed' => TRUE,
  494. '#tree' => TRUE,
  495. );
  496. $form['legendset']['legend'] = $legend;
  497. // Add config forms and remove flags to mappings.
  498. $form['config'] = $form['remove_flags'] = $form['mapping_weight'] = array(
  499. '#tree' => TRUE,
  500. );
  501. if (is_array($mappings)) {
  502. $delta = count($mappings) + 2;
  503. foreach ($mappings as $i => $mapping) {
  504. if (isset($targets[$mapping['target']])) {
  505. $form['config'][$i] = feeds_ui_mapping_settings_form($form, $form_state, $i, $mapping, $targets[$mapping['target']]);
  506. }
  507. $form['remove_flags'][$i] = array(
  508. '#type' => 'checkbox',
  509. '#title' => t('Remove'),
  510. '#prefix' => '<div class="feeds-ui-checkbox-link">',
  511. '#suffix' => '</div>',
  512. );
  513. $form['mapping_weight'][$i] = array(
  514. '#type' => 'weight',
  515. '#title' => '',
  516. '#default_value' => $i,
  517. '#delta' => $delta,
  518. '#attributes' => array(
  519. 'class' => array(
  520. 'feeds-ui-mapping-weight'
  521. ),
  522. ),
  523. );
  524. }
  525. }
  526. if (isset($source_options)) {
  527. $form['source'] = array(
  528. '#type' => 'select',
  529. '#title' => t('Source'),
  530. '#title_display' => 'invisible',
  531. '#options' => $source_options,
  532. '#empty_option' => t('- Select a source -'),
  533. '#description' => t('An element from the feed.'),
  534. );
  535. }
  536. else {
  537. $form['source'] = array(
  538. '#type' => 'textfield',
  539. '#title' => t('Source'),
  540. '#title_display' => 'invisible',
  541. '#size' => 20,
  542. '#default_value' => '',
  543. '#description' => t('The name of source field.'),
  544. );
  545. }
  546. $form['target'] = array(
  547. '#type' => 'select',
  548. '#title' => t('Target'),
  549. '#title_display' => 'invisible',
  550. '#options' => $target_options,
  551. '#empty_option' => t('- Select a target -'),
  552. '#description' => t('The field that stores the data.'),
  553. );
  554. $form['actions'] = array('#type' => 'actions');
  555. $form['actions']['save'] = array(
  556. '#type' => 'submit',
  557. '#value' => t('Save'),
  558. );
  559. return $form;
  560. }
  561. /**
  562. * Per mapper configuration form that is a part of feeds_ui_mapping_form().
  563. */
  564. function feeds_ui_mapping_settings_form($form, $form_state, $i, $mapping, $target) {
  565. $form_state += array(
  566. 'mapping_settings_edit' => NULL,
  567. 'mapping_settings' => array(),
  568. );
  569. $base_button = array(
  570. '#submit' => array('feeds_ui_mapping_form_multistep_submit'),
  571. '#ajax' => array(
  572. 'callback' => 'feeds_ui_mapping_settings_form_callback',
  573. 'wrapper' => 'feeds-ui-mapping-form-wrapper',
  574. 'effect' => 'fade',
  575. 'progress' => 'none',
  576. ),
  577. '#i' => $i,
  578. );
  579. if (isset($form_state['mapping_settings'][$i])) {
  580. $mapping = $form_state['mapping_settings'][$i] + $mapping;
  581. }
  582. if ($form_state['mapping_settings_edit'] === $i) {
  583. $settings_form = array();
  584. foreach ($target['form_callbacks'] as $callback) {
  585. $settings_form += call_user_func($callback, $mapping, $target, $form, $form_state);
  586. }
  587. // Merge in the optional unique form.
  588. $settings_form += feeds_ui_mapping_settings_optional_unique_form($mapping, $target, $form, $form_state);
  589. return array(
  590. '#type' => 'container',
  591. 'settings' => $settings_form,
  592. 'save_settings' => $base_button + array(
  593. '#type' => 'submit',
  594. '#name' => 'mapping_settings_update_' . $i,
  595. '#value' => t('Update'),
  596. '#op' => 'update',
  597. ),
  598. 'cancel_settings' => $base_button + array(
  599. '#type' => 'submit',
  600. '#name' => 'mapping_settings_cancel_' . $i,
  601. '#value' => t('Cancel'),
  602. '#op' => 'cancel',
  603. ),
  604. );
  605. }
  606. else {
  607. // Build the summary.
  608. $summary = array();
  609. foreach ($target['summary_callbacks'] as $callback) {
  610. $summary[] = call_user_func($callback, $mapping, $target, $form, $form_state);
  611. }
  612. // Filter out empty summary values.
  613. $summary = implode('<br />', array_filter($summary));
  614. // Append the optional unique summary.
  615. if ($optional_unique_summary = feeds_ui_mapping_settings_optional_unique_summary($mapping, $target, $form, $form_state)) {
  616. $summary .= ' ' . $optional_unique_summary;
  617. }
  618. if ($summary) {
  619. return array(
  620. 'summary' => array(
  621. '#prefix' => '<div>',
  622. '#markup' => $summary,
  623. '#suffix' => '</div>',
  624. ),
  625. 'edit_settings' => $base_button + array(
  626. '#type' => 'image_button',
  627. '#name' => 'mapping_settings_edit_' . $i,
  628. '#src' => 'misc/configure.png',
  629. '#attributes' => array('alt' => t('Edit')),
  630. '#op' => 'edit',
  631. ),
  632. );
  633. }
  634. }
  635. return array();
  636. }
  637. /**
  638. * Submit callback for a per mapper configuration form. Switches between edit
  639. * and summary mode.
  640. */
  641. function feeds_ui_mapping_form_multistep_submit($form, &$form_state) {
  642. $trigger = $form_state['triggering_element'];
  643. switch ($trigger['#op']) {
  644. case 'edit':
  645. $form_state['mapping_settings_edit'] = $trigger['#i'];
  646. break;
  647. case 'update':
  648. $values = $form_state['values']['config'][$trigger['#i']]['settings'];
  649. $form_state['mapping_settings'][$trigger['#i']] = $values;
  650. unset($form_state['mapping_settings_edit']);
  651. break;
  652. case 'cancel':
  653. unset($form_state['mapping_settings_edit']);
  654. break;
  655. }
  656. $form_state['rebuild'] = TRUE;
  657. }
  658. /**
  659. * AJAX callback that returns the whole feeds_ui_mapping_form().
  660. */
  661. function feeds_ui_mapping_settings_form_callback($form, $form_state) {
  662. return $form;
  663. }
  664. /**
  665. * Validation handler for feeds_ui_mapping_form().
  666. */
  667. function feeds_ui_mapping_form_validate($form, &$form_state) {
  668. if (!strlen($form_state['values']['source']) xor !strlen($form_state['values']['target'])) {
  669. // Check triggering_element here so we can react differently for ajax
  670. // submissions.
  671. switch ($form_state['triggering_element']['#name']) {
  672. // Regular form submission.
  673. case 'op':
  674. if (!strlen($form_state['values']['source'])) {
  675. form_error($form['source'], t('You must select a mapping source.'));
  676. }
  677. else {
  678. form_error($form['target'], t('You must select a mapping target.'));
  679. }
  680. break;
  681. // Be more relaxed on ajax submission.
  682. default:
  683. form_set_value($form['source'], '', $form_state);
  684. form_set_value($form['target'], '', $form_state);
  685. break;
  686. }
  687. }
  688. }
  689. /**
  690. * Submission handler for feeds_ui_mapping_form().
  691. */
  692. function feeds_ui_mapping_form_submit($form, &$form_state) {
  693. $importer = feeds_importer($form['#importer']);
  694. $processor = $importer->processor;
  695. $form_state += array(
  696. 'mapping_settings' => array(),
  697. 'mapping_settings_edit' => NULL,
  698. );
  699. // If an item is in edit mode, prepare it for saving.
  700. if ($form_state['mapping_settings_edit'] !== NULL) {
  701. $values = $form_state['values']['config'][$form_state['mapping_settings_edit']]['settings'];
  702. $form_state['mapping_settings'][$form_state['mapping_settings_edit']] = $values;
  703. }
  704. // We may set some settings to mappings that we remove in the subsequent step,
  705. // that's fine.
  706. $mappings = $form['#mappings'];
  707. foreach ($form_state['mapping_settings'] as $k => $v) {
  708. $mappings[$k] = array(
  709. 'source' => $mappings[$k]['source'],
  710. 'target' => $mappings[$k]['target'],
  711. ) + $v;
  712. }
  713. if (!empty($form_state['values']['remove_flags'])) {
  714. $remove_flags = array_keys(array_filter($form_state['values']['remove_flags']));
  715. foreach ($remove_flags as $k) {
  716. unset($mappings[$k]);
  717. unset($form_state['values']['mapping_weight'][$k]);
  718. drupal_set_message(t('Mapping has been removed.'), 'status', FALSE);
  719. }
  720. }
  721. // Keep our keys clean.
  722. $mappings = array_values($mappings);
  723. if (!empty($mappings)) {
  724. array_multisort($form_state['values']['mapping_weight'], $mappings);
  725. }
  726. $processor->addConfig(array('mappings' => $mappings));
  727. if (strlen($form_state['values']['source']) && strlen($form_state['values']['target'])) {
  728. try {
  729. $mappings = $processor->getMappings();
  730. $mappings[] = array(
  731. 'source' => $form_state['values']['source'],
  732. 'target' => $form_state['values']['target'],
  733. 'unique' => FALSE,
  734. );
  735. $processor->addConfig(array('mappings' => $mappings));
  736. drupal_set_message(t('Mapping has been added.'));
  737. }
  738. catch (Exception $e) {
  739. drupal_set_message($e->getMessage(), 'error');
  740. }
  741. }
  742. $importer->save();
  743. drupal_set_message(t('Your changes have been saved.'));
  744. }
  745. /**
  746. * Walk the result of FeedsParser::getMappingSources() or
  747. * FeedsProcessor::getMappingTargets() and format them into
  748. * a Form API options array.
  749. */
  750. function _feeds_ui_format_options($options, $show_deprecated = FALSE) {
  751. $result = array();
  752. foreach ($options as $k => $v) {
  753. if (!$show_deprecated && is_array($v) && !empty($v['deprecated'])) {
  754. continue;
  755. }
  756. if (is_array($v) && !empty($v['name'])) {
  757. $result[$k] = $v['name'] . ' (' . $k . ')';
  758. if (!empty($v['deprecated'])) {
  759. $result[$k] .= ' - ' . t('DEPRECATED');
  760. }
  761. }
  762. elseif (is_array($v)) {
  763. $result[$k] = $k;
  764. }
  765. else {
  766. $result[$k] = $v;
  767. }
  768. }
  769. asort($result);
  770. return $result;
  771. }
  772. /**
  773. * Per mapping settings summary callback. Shows whether a mapping is used as
  774. * unique or not.
  775. */
  776. function feeds_ui_mapping_settings_optional_unique_summary($mapping, $target, $form, $form_state) {
  777. if (!empty($target['optional_unique'])) {
  778. if ($mapping['unique']) {
  779. return t('Used as <strong>unique</strong>.');
  780. }
  781. else {
  782. return t('Not used as unique.');
  783. }
  784. }
  785. }
  786. /**
  787. * Per mapping settings form callback. Lets the user choose if a target is as
  788. * unique or not.
  789. */
  790. function feeds_ui_mapping_settings_optional_unique_form($mapping, $target, $form, $form_state) {
  791. $settings_form = array();
  792. if (!empty($target['optional_unique'])) {
  793. $settings_form['unique'] = array(
  794. '#type' => 'checkbox',
  795. '#title' => t('Unique'),
  796. '#default_value' => !empty($mapping['unique']),
  797. );
  798. }
  799. return $settings_form;
  800. }
  801. /**
  802. * Theme feeds_ui_overview_form().
  803. */
  804. function theme_feeds_ui_overview_form($variables) {
  805. $form = $variables['form'];
  806. drupal_add_css(drupal_get_path('module', 'feeds_ui') . '/feeds_ui.css');
  807. // Iterate through all importers and build a table.
  808. $rows = array();
  809. foreach (array('enabled', 'disabled') as $type) {
  810. if (isset($form[$type])) {
  811. foreach (element_children($form[$type]) as $id) {
  812. $row = array();
  813. foreach (element_children($form[$type][$id]) as $col) {
  814. $row[$col] = array(
  815. 'data' => drupal_render($form[$type][$id][$col]),
  816. 'class' => array($type),
  817. );
  818. }
  819. $rows[] = array(
  820. 'data' => $row,
  821. 'class' => array($type),
  822. );
  823. }
  824. }
  825. }
  826. $output = theme('table', array(
  827. 'header' => $form['#header'],
  828. 'rows' => $rows,
  829. 'attributes' => array('class' => array('feeds-admin-importers')),
  830. 'empty' => t('No importers available.'),
  831. ));
  832. if (!empty($rows)) {
  833. $output .= drupal_render_children($form);
  834. }
  835. return $output;
  836. }
  837. /**
  838. * Theme feeds_ui_edit_page().
  839. */
  840. function theme_feeds_ui_edit_page($variables) {
  841. $config_info = $variables['info'];
  842. $active_container = $variables['active'];
  843. drupal_add_css(drupal_get_path('module', 'feeds_ui') . '/feeds_ui.css');
  844. // Outer wrapper.
  845. $output = '<div class="feeds-settings clearfix">';
  846. // Build left bar.
  847. $output .= '<div class="left-bar">';
  848. foreach ($config_info as $info) {
  849. $output .= theme('feeds_ui_container', array('container' => $info));
  850. }
  851. $output .= '</div>';
  852. // Build configuration space.
  853. $output .= '<div class="configuration">';
  854. $output .= '<div class="configuration-squeeze">';
  855. $output .= theme('feeds_ui_container', array('container' => $active_container));
  856. $output .= '</div>';
  857. $output .= '</div>';
  858. $output .= '</div>'; // ''<div class="feeds-settings">';
  859. return $output;
  860. }
  861. /**
  862. * Render a simple container. A container can have a title, a description and
  863. * one or more actions. Recursive.
  864. *
  865. * @todo Replace with theme_fieldset or a wrapper to theme_fieldset?
  866. *
  867. * @param $variables
  868. * An array containing an array at 'container'.
  869. * A 'container' array may contain one or more of the following keys:
  870. * array(
  871. * 'title' => 'the title',
  872. * 'body' => 'the body of the container, may also be an array of more
  873. * containers or a renderable array.',
  874. * 'class' => array('the class of the container.'),
  875. * 'id' => 'the id of the container',
  876. * );
  877. */
  878. function theme_feeds_ui_container($variables) {
  879. $container = $variables['container'];
  880. $class = array_merge(array('feeds-container'), empty($container['class']) ? array('plain') : $container['class']);
  881. $id = empty($container['id']) ? '': ' id="' . $container['id'] . '"';
  882. $output = '<div class="' . implode(' ', $class) . '"' . $id . '>';
  883. if (isset($container['actions']) && count($container['actions'])) {
  884. $output .= '<ul class="container-actions">';
  885. foreach ($container['actions'] as $action) {
  886. $output .= '<li>' . $action . '</li>';
  887. }
  888. $output .= '</ul>';
  889. }
  890. if (!empty($container['title'])) {
  891. $output .= '<h4 class="feeds-container-title">';
  892. $output .= $container['title'];
  893. $output .= '</h4>';
  894. }
  895. if (!empty($container['body'])) {
  896. $output .= '<div class="feeds-container-body">';
  897. if (is_array($container['body'])) {
  898. if (isset($container['body']['#type'])) {
  899. $output .= drupal_render($container['body']);
  900. }
  901. else {
  902. foreach ($container['body'] as $c) {
  903. $output .= theme('feeds_ui_container', array('container' => $c));
  904. }
  905. }
  906. }
  907. else {
  908. $output .= $container['body'];
  909. }
  910. $output .= '</div>';
  911. }
  912. $output .= '</div>';
  913. return $output;
  914. }
  915. /**
  916. * Theme function for feeds_ui_mapping_form().
  917. */
  918. function theme_feeds_ui_mapping_form($variables) {
  919. $form = $variables['form'];
  920. $targets = feeds_importer($form['#importer'])->processor->getMappingTargets();
  921. $targets = _feeds_ui_format_options($targets, TRUE);
  922. $sources = feeds_importer($form['#importer'])->parser->getMappingSources();
  923. // Some parsers do not define source options.
  924. $sources = $sources ? _feeds_ui_format_options($sources, TRUE) : array();
  925. // Build the actual mapping table.
  926. $header = array(
  927. t('Source'),
  928. t('Target'),
  929. t('Target configuration'),
  930. '&nbsp;',
  931. t('Weight'),
  932. );
  933. $rows = array();
  934. if (is_array($form['#mappings'])) {
  935. foreach ($form['#mappings'] as $i => $mapping) {
  936. $source = isset($sources[$mapping['source']]) ? check_plain($sources[$mapping['source']]) : check_plain($mapping['source']);
  937. $target = isset($targets[$mapping['target']]) ? check_plain($targets[$mapping['target']]) : '<em>' . t('Missing') . '</em>';
  938. // Add indicator to target if target configuration changed.
  939. if (isset($form['#mapping_settings'][$i])) {
  940. $target .= '<span class="warning">*</span>';
  941. }
  942. $rows[] = array(
  943. 'data' => array(
  944. $source,
  945. $target,
  946. drupal_render($form['config'][$i]),
  947. drupal_render($form['remove_flags'][$i]),
  948. drupal_render($form['mapping_weight'][$i]),
  949. ),
  950. 'class' => array('draggable', 'tabledrag-leaf'),
  951. );
  952. }
  953. }
  954. if (!count($rows)) {
  955. $rows[] = array(
  956. array(
  957. 'colspan' => 5,
  958. 'data' => t('No mappings defined.'),
  959. ),
  960. );
  961. }
  962. $rows[] = array(
  963. drupal_render($form['source']),
  964. drupal_render($form['target']),
  965. '',
  966. drupal_render($form['add']),
  967. '',
  968. );
  969. $output = '';
  970. if (!empty($form['changed'])) {
  971. // This form element is only available if target configuration changed.
  972. $output .= drupal_render($form['changed']);
  973. }
  974. $output .= '<div class="help feeds-admin-ui">' . drupal_render($form['help']) . '</div>';
  975. $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'feeds-ui-mapping-overview')));
  976. // Build the help table that explains available sources.
  977. $legend = '';
  978. $rows = array();
  979. foreach (element_children($form['legendset']['legend']['sources']) as $k) {
  980. $rows[] = array(
  981. check_plain(drupal_render($form['legendset']['legend']['sources'][$k]['name'])),
  982. check_plain(drupal_render($form['legendset']['legend']['sources'][$k]['description'])),
  983. );
  984. }
  985. if (count($rows)) {
  986. $legend .= '<h4>' . t('Sources') . '</h4>';
  987. $legend .= theme('table', array('header' => array(t('Name'), t('Description')), 'rows' => $rows));
  988. }
  989. // Build the help table that explains available targets.
  990. $rows = array();
  991. foreach (element_children($form['legendset']['legend']['targets']) as $k) {
  992. $rows[] = array(
  993. check_plain(drupal_render($form['legendset']['legend']['targets'][$k]['name']) . ' (' . $k . ')'),
  994. check_plain(drupal_render($form['legendset']['legend']['targets'][$k]['description'])),
  995. );
  996. }
  997. $legend .= '<h4>' . t('Targets') . '</h4>';
  998. $legend .= theme('table', array('header' => array(t('Name'), t('Description')), 'rows' => $rows));
  999. // Stick tables into collapsible fieldset.
  1000. $form['legendset']['legend'] = array(
  1001. '#markup' => '<div>' . $legend . '</div>',
  1002. );
  1003. $output .= drupal_render($form['legendset']);
  1004. $output .= drupal_render_children($form);
  1005. drupal_add_tabledrag('feeds-ui-mapping-overview', 'order', 'sibling', 'feeds-ui-mapping-weight');
  1006. return $output;
  1007. }
  1008. /**
  1009. * Page callback to import a Feeds importer.
  1010. */
  1011. function feeds_ui_importer_import($form, &$form_state) {
  1012. $form['id'] = array(
  1013. '#type' => 'textfield',
  1014. '#title' => t('Importer id'),
  1015. '#description' => t('Enter the id to use for this importer if it is different from the source importer. Leave blank to use the id of the importer.'),
  1016. );
  1017. $form['id_override'] = array(
  1018. '#type' => 'checkbox',
  1019. '#title' => t('Replace an existing importer if one exists with the same id.'),
  1020. );
  1021. $form['bypass_validation'] = array(
  1022. '#type' => 'checkbox',
  1023. '#title' => t('Bypass importer validation'),
  1024. '#description' => t('Bypass the validation of plugins when importing.'),
  1025. );
  1026. $form['importer'] = array(
  1027. '#type' => 'textarea',
  1028. '#rows' => 10,
  1029. );
  1030. $form['actions'] = array('#type' => 'actions');
  1031. $form['actions']['submit'] = array(
  1032. '#type' => 'submit',
  1033. '#value' => t('Import'),
  1034. );
  1035. return $form;
  1036. }
  1037. /**
  1038. * Form validation handler for feeds_ui_importer_import().
  1039. *
  1040. * @see feeds_ui_importer_import_submit()
  1041. */
  1042. function feeds_ui_importer_import_validate($form, &$form_state) {
  1043. $form_state['values']['importer'] = trim($form_state['values']['importer']);
  1044. $form_state['values']['id'] = trim($form_state['values']['id']);
  1045. if (!empty($form_state['values']['id']) && preg_match('/[^a-zA-Z0-9_]/', $form_state['values']['id'])) {
  1046. form_error($form['id'], t('Feeds importer id must be alphanumeric with underscores only.'));
  1047. }
  1048. if (substr($form_state['values']['importer'], 0, 5) == '<?php') {
  1049. $form_state['values']['importer'] = substr($form_state['values']['importer'], 5);
  1050. }
  1051. $feeds_importer = NULL;
  1052. ob_start();
  1053. eval($form_state['values']['importer']);
  1054. ob_end_clean();
  1055. if (!is_object($feeds_importer)) {
  1056. return form_error($form['importer'], t('Unable to interpret Feeds importer code.'));
  1057. }
  1058. if (empty($feeds_importer->api_version) || $feeds_importer->api_version < 1) {
  1059. form_error($form['importer'], t('The importer is not compatible with this version of Feeds.'));
  1060. }
  1061. elseif (version_compare($feeds_importer->api_version, feeds_api_version(), '>')) {
  1062. form_error($form['importer'], t('That importer is created for the version %import_version of Feeds, but you only have version %api_version.', array(
  1063. '%import_version' => $feeds_importer->api_version,
  1064. '%api_version' => feeds_api_version())));
  1065. }
  1066. // Change to user-supplied id.
  1067. if ($form_state['values']['id']) {
  1068. $feeds_importer->id = $form_state['values']['id'];
  1069. }
  1070. $exists = feeds_ui_importer_machine_name_exists($feeds_importer->id);
  1071. if ($exists && !$form_state['values']['id_override']) {
  1072. if (feeds_importer($feeds_importer->id)->export_type != EXPORT_IN_CODE) {
  1073. return form_error($form['id'], t('Feeds importer already exists with that id.'));
  1074. }
  1075. }
  1076. if (!$form_state['values']['bypass_validation']) {
  1077. foreach (array('fetcher', 'parser', 'processor') as $type) {
  1078. $plugin = feeds_plugin($feeds_importer->config[$type]['plugin_key'], $feeds_importer->id);
  1079. if (get_class($plugin) == 'FeedsMissingPlugin') {
  1080. form_error($form['importer'], t('The plugin %plugin is unavailable.', array('%plugin' => $feeds_importer->config[$type]['plugin_key'])));
  1081. }
  1082. }
  1083. }
  1084. $form_state['importer'] = $feeds_importer;
  1085. }
  1086. /**
  1087. * Form submission handler for feeds_ui_importer_import().
  1088. *
  1089. * @see feeds_ui_importer_import_validate()
  1090. */
  1091. function feeds_ui_importer_import_submit($form, &$form_state) {
  1092. $importer = $form_state['importer'];
  1093. // Create a copy of the importer to preserve config.
  1094. $save = feeds_importer($importer->id);
  1095. $save->setConfig($importer->config);
  1096. foreach (array('fetcher', 'parser', 'processor') as $type) {
  1097. $save->setPlugin($importer->config[$type]['plugin_key']);
  1098. $save->$type->setConfig($importer->config[$type]['config']);
  1099. }
  1100. $save->save();
  1101. drupal_set_message(t('Successfully imported the %id feeds importer.', array('%id' => $importer->id)));
  1102. $form_state['redirect'] = 'admin/structure/feeds';
  1103. }