diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..723ef36f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a591562a..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,128 +0,0 @@ -## Changelog - -#### 1.4.0 - -- Craft 2.5 support, including release feed and icons. -- Code cleanup and refactoring field-mapping logic for performance and sanity. -- Rewritten Matrix/Table mapping and processing logic. **Matrix and Table fields will need to be re-mapped**. -- Fix for mapping multiple Matrix blocks being out of order from original feed. -- Removed Super Table native support - please ensure you have the 0.4.0 release of Super Table. **Super Table fields will need to be re-mapped**. -- Modified third-party hooks `prepForFeedMeFieldType` so it actually works! Thanks go to [@lindseydiloreto](https://github.com/lindseydiloreto). -- Added `registerFeedMeMappingOptions` for third-party fieldtypes to control the options for mapping feed nodes to field data. -- Added `postForFeedMeFieldType` for third-party fieldtypes to modify entry data before being saved to entry. -- Added documentation for hooks. Refer to [Wiki](https://github.com/engram-design/FeedMe/wiki/Hooks). -- Less strict user matching - should match against almost any value related to user. -- Allow for Environment Variables to be used in the feed url. - -#### 1.3.6 - -- Removed `file_get_contents` as default method of fetching feed data in favour of Curl. -- Better error logging when trying to consume feed data. -- Fix for when mapping to Matrix field, commas were escaping content into new blocks. -- Ensure fields within Matrix and SuperTable are parsed through necessary field processing functions. -- Added `prepForFeedMeFieldType` hook for other plugins to handle their own fields. - -#### 1.3.5 - -- Minor fix for logging. When Delete duplication option was set, import success was never recorded in the logs. - -#### 1.3.4 - -- Minor fix for template mapping. Caused an issue when using a JSON feed and came across a empty nested array. - -#### 1.3.3 - -- Minor fix for log reporting which wasn't displaying errors in a useful way. - -#### 1.3.2 - -- Alterations to logging information to provide better feedback. Thanks to [@russback](https://github.com/russback). - -#### 1.3.1 - -- Fix for info/notice log messages not saving when `devMode` is off. - -#### 1.3.0 - -- Refactored direct processing to utalize Craft's tasks service, rather than using pure PHP processing. This greatly improves performance as PHP processing would run out of memory extremely quickly. - -#### 1.2.9 - -- Added support for [SuperTable](https://github.com/engram-design/SuperTable). -- Added log tab to read in `craft/storage/runtime/logs/feedme.log`. -- Added help tab, allowing users to submit their feed info and setup for debugging/troubleshooting. -- Fix for fields in Matrix blocks only succesfully mapping textual fields. Complex fields such as Assets, Entries, etc were not mapping correctly. -- Fix for only one item being processed when Delete duplication handling was selected. -- Fix for Dropdown/RadioButtons causing a critical error when a provided value didn't exist in the field. -- Added credit and plugin url to footer. - -#### 1.2.8 - -- Move changelog - so much change! -- Add support for attribute values for XML feeds (template tags only). -- Add missing log statement for successful update/add. - -#### 1.2.7 - -- Fix where entries would not import if mapping element fields had more values that their field limit. -- Fix for multiple matches found on existing categories, where only one should match. -- Fix for escaping special characters in tags/category name. -- Minor fix for tags/category mapping. - -#### 1.2.6 - -- Fix for matching fields containing special characters. -- Fix for tags and category mapping, mapping all available if supplied empty value. -- Fix for backup lightswitch reflecting the saved state. -- Fix to ensure at least one duplicate field is checked. - -#### 1.2.5 - -- Refactoring for performance improvements. -- Remove database logging until a better performing option is figured out. Logging still occurs to the file system under `craft/storage/runtime/logs/`. -- Added optional backup option per-feed (default to true). -- Minor fix so direct feed link doesn't use `siteUrl`. - -#### 1.2.4 - -- Added support to fallback on cURL when `file_get_contents()` is not available. Can occur on some hosts where `allow_url_fopen = 0`. - -#### 1.2.3 - -- Primary Element is no longer required - important for JSON feeds. -- Fixes for when no primary element specified. It's pretty much optional now. -- UI tidy for mapping table. -- Fix for duplication handling not matching in some cases. Now uses element search. - -#### 1.2.2 - -- JSON feed support. - -#### 1.2.1 - -- Matrix support. -- Table support. -- Even better element-search. -- Remove square brackets for nested field - serialization issues. **Breaking change** you will need to re-map some fields due to this fix. -- Fix for supporting multiple entry types when selecting fields to map. - -#### 1.2 - -- Lots of fixes and improvements for direct-processing. Includes URL parameter, passkey and non-Task processing. -- Fixes with logging - now more informative! -- Improvement nested element parsing. -- Better date parsing. -- CSRF protection compatibility. -- Fix for duplicate field mapping not being remembered. - - -#### 1.1 - -- Prep for Table/Matrix mapping. -- Better depth-mapping for feed data (was limited to depth-2). -- Refactor field-mapping processing. -- Set minimum Craft build. - -#### 1.0 - -- Initial release. diff --git a/README.md b/README.md index e28b7d57..1d6c3651 100644 --- a/README.md +++ b/README.md @@ -1,266 +1,34 @@ -# Feed Me 2.0 - Public Beta - -We've released the public beta for Feed Me 2.0.0, along with a brand new plugin site via http://sgroup.com.au/plugins/feedme/getting-started/updates. - # Feed Me -Feed Me is a Craft plugin to make it easy to import entries and entry data from XML, RSS, ATOM or JSON feeds. Feeds can be setup as a task in Craft's Control Panel, or called on-demand for use in your twig templates. - -A common use-case for this plugin is to consume external feeds (news, events), but can also be used as a once-off task for importing content when migrating from other sites. - - +Feed Me is a Craft plugin for super-simple importing of content, either once-off or at regular intervals. With support for XML, RSS, ATOM or JSON feeds, you'll be able to import your content as Entries, Categories, Craft Commerce Products (and variants), and more. + ## Features -- Import data from XML, RSS, ATOM or JSON feeds. +- Import data from XML, RSS, ATOM or JSON feeds, local or remote. +- Import into Entries (Free), Categories, Users, and Commerce Products (Pro only). - Feeds are saved to allow easy re-processing on-demand, or to be used in a Cron job. -- Map feed data to your entry fields. See Supported Fieldtypes. +- Simple field-mapping interface to match your feed data with your element fields. - Duplication handling - control what happens when feeds are processed again. - Uses Craft's Task service to process feeds in the background. - Database backups before each feed processing. - Troubleshoot feed processing issues with logs. - Grab feed data directly from your twig templates. -- Craft 2.5 compatible. - - -## Install - -- Add the `feedme` directory into your `craft/plugins` directory. -- Navigate to Settings -> Plugins and click the "Install" button. - -**Plugin options** - -- Change the plugin name as it appear in the CP navigation. -- Set the default cache (for calls using the template tag only). -- Enable or disable specific tabs for Feed Me. - - -## Usage - -Head to the Feed Me page using the CP navigation, and click the New Feed button. - -Enter the required details to configure your feed: - -- Name this feed something useful so you'll remember what it does. -- The actual URL to your feed. -- Set the Feed Type to assist with mapping the correct elements. -- The Primary Element reflects which node in the feed your data sits. -- Select the Section and Entry Type for where you want to put the feed data. -- Decide on an import strategy: how you'd like to handle duplicate feed items (if you're going to be re-running this feed). - - - -Click on the `Save and continue` to be taken to the field mapping screen. Here, you select what data from the feed you wish to capture, and what fields to map it to, depending on your Section and Entry Type selection. Here you'll be able to choose which fields (can be more than one) you'd like to compare to determine if a feed item is a duplicate. - -You must map data to at least the Title field, or any other required field for your entry. - - - -Save the feed for later, or start the import. - - -### Supported Fieldtypes - -Feed Me supports mapping data from your feeds to the following Fieldtypes: - -**Craft** - -- Assets -- Categories -- Checkboxes -- Color -- Date/Time -- Dropdown -- Entries -- Lightswitch -- Matrix -- Multi-Select -- Number -- Plain Text -- Position Select -- Radio Buttons -- Rich Text -- Table -- Tags -- Users - -**Third-Party** - -- Super Table -- SmartMap - - -###Element Creation - -For certain elements, it may be benefitial to create the element's data, if not already created. Like if an Asset doesn't exist in your Assets collection, upload it. Similarly with Categories, and other fields. - -Currently, Feed Me handles the following applicable fields in these ways: - -**Assets:** Only supports mapping existing assets to this entry. - -**Categories:** Are created if they do not exist, or mapped if they do. - -**Entries:** Only supports mapping existing entries to this entry. The feed field must contain either the Title or Slug of the entry to successfully map. - -**Tags:** Are always created. - -**Users:** Only supports mapping existing users to this entry. - -Internally, Feed Me uses Craft's element search to match against the value in your feed for an element. For example, if you have `my_filename.png` as a value in your feed, and you are mapping to an Asset, ensure that searching through the Assets index screen actually returns what you expect. - -For troubleshooting, ensure you have completed the Rebuild Search Indexes task. - -We plan to include options for whether you would like to do this on a per-field basis. - - -### Import strategy - -When running the feed task multiple times, there may or may not be the same data present in the feed as the last time you ran the task. To deal with this, you can control what happens to old (and new) entries when the feed is processed again. - -You may choose multiple fields to determine if an entry is a duplicate. Most commonly, you'll want to compare the `Title` field, but can be any fields you require. - -#### Strategy options: - -**Add Entries:** - -Existing entries will be skipped and left untouched, new entries however, will be added to the section. Use case: Feed aggregation, blog entries, etc. - -_"I want to keep existing entries untouched but add new ones."_ - -**Update Entries** - -Existing entries will have their fields updated with data from this feed. Use case: Any feed which needs to be kept up to date. - -_"I want to update existing entries and add new ones."_ - -**Delete Entries** - -Delete all existing entries in this section, adding only entries from this feed. **Be careful.** Use case: Events, or when only data from the current feed is required. - -_"I want only the entries from this feed in this section."_ - - -### Using with a Cron job - -Scheduling feed processing is not something thats currently built into Feed Me. Instead, you'll need to setup a Cron job, or a similar scheduled task to fire the feed processing at the desired interval. - -Find the 'Direct feed link' icon (next to the delete icon) on the main Feed Me page and copy this URL. Use one of the following to setup as a Cron Job - replacing the URL with what you just copied. - -``` -/usr/bin/wget -O - -q -t 1 "http://your.domain/actions/feedMe/feeds/runTask?direct=1&feedId=1&passkey=FwafY5kg3c" - -curl --silent --compressed "http://your.domain/actions/feedMe/feeds/runTask?direct=1&feedId=1&passkey=FwafY5kg3c" - -/usr/bin/lynx -source "http://your.domain/actions/feedMe/feeds/runTask?direct=1&feedId=1&passkey=FwafY5kg3c" -``` - -### Parameters - -- `direct` _(required)_ - Must be set to `1` or `true`. Tells Feed Me this is a externally-triggered task. -- `feedId` _(required)_ - The ID of the feed you wish to process. -- `passkey` _(required)_ - A unique, generated identifier for this feed. Ensures not just anyone can trigger the import. -- `url` _(optional)_ - If your feed URL changes, you can specify it here. Ensure the structure of the feed matches your field mappings. - - -### Performance - -Feed Me can handle importing large feeds by using Craft's Tasks service. Testing has shown that processing a feed with 10K items take roughly 15 minutes to import. - -To get the most out of your feed processing, follow the below suggestions: - -- Turn off `devMode`. Craft's built-in logging when `devMode` is switched on will greatly slow down the import process, and causes a high degree of memory overhead. -- Consider selecting the `Add Entries` option for duplication handling, depending on your requirements. -- Consider turning off the `Backup` option for the feed. This will depend on your specific circumstances. - -You may also need to adjust the `memory_limit` and `max_execution_time` values in your php.ini file if you run into memory issues. - - -## Template example - -While you can create a feed task to insert data as entries, there are times which you may prefer to capture feed data on-demand, rather than saving as an entry. You can easily do this through your twig templates using the below. - -Feeds are cached for performance (default to 60 seconds), which can be set by a tag parameter, or in the plugin settings. - - {% set params = { - url: 'http://path.to/feed/', - type: 'xml', - element: 'item', - cache: 60, - } %} - - {% set feed = craft.feedme.feed(params) %} - - {% for node in feed %} - Title: {{ node.title }} - Publish Date: {{ node.pubDate }} - Content: {{ node['content:encoded'] }} - - {% for name in node.category %} - Category: {{ name }} - {% endfor %} - {% endfor %} - -### Template parameters - -- `url` _(string, required)_ - URL to the feed. -- `type` _(string, optional)_ - The type of feed you're fetching data from. Valid options are `json` or `xml` (defaults to `xml`). -- `element` _(string, optional)_ - Element to start feed from. Useful for deep feeds. -- `cache` _(bool or number, optional)_ - Whether or not to cache the request. If `true`, will use the default as set in the plugin settings, or if a number, will use that as its duration. Setting to `false` will disable cache completely. - -For XML-based feeds, you will also have access to all attributes for a particular node. These are accessible through the `attributes` keyword. For example, the XML `Another Value`, you can use `{{ xml.field.attributes.my_attribute }}`. - -If you're looking to consume REST feeds, APIs or other third-party platforms (Facebook, Twitter, etc), I would highly recommend using [alecritson's Placid](https://github.com/alecritson/Placid) plugin, which supports a great deal more than this plugin offers. - - -## Hooks - -For third-party field type integration, consult the [Wiki](https://github.com/engram-design/FeedMe/wiki/Hooks). - - -## Roadmap - -- Improve mapping by: - - More refined and flexible mapping interface. - - Support importing into specific locale's. - - Allow for default value to be set. - - Finer control over category/tag creation - - Support uploading of assets. - - Full support (creation) for search-only field types (Assets, Entries, Users) - - Attribute-mapping support for XML feeds - - Wildcard node names (for non-consistent node names) -- Allow feed processing to be reverted -- Support authentication for feed access (Basic, OAuth, Token) -- Organise documentation into Wiki -- Fix issue with mapping of Assets and the source not being set to All (not reproducible). -- Write up full tutorial from start to finish (JSON/XML). - -Have a suggestion? We'd love to hear about it! [Make a suggestion](https://github.com/engram-design/FeedMe/issues) - - -## Release Notes - -Below are major release notes when updating from one version to another. Breaking changes will be listed here. - -- [Updating from 1.4.0](https://github.com/engram-design/FeedMe/wiki/Release-Notes#updating-from-140) - - -## Support - -If you're having an issue using Feed Me, the best course of action is to send us a message through the support form on the Help tab. This will provide us with enough detail to assist. +- Craft 2.5+ compatible. - -Otherwise, either [Submit an issue](https://github.com/engram-design/FeedMe/issues) or ask for assistance in the [Craft Slack Group](https://buildwithcraft.com/community#slack). +## Documentation +Visit our [Plugin page](http://sgroup.com.au/plugins/feedme) for all documentation, a getting started guide, template tags, and developer resources. ## Thanks / Contributions -A massive thanks to [Bob Olde Hampsink](https://github.com/boboldehampsink) and his amazing work on the [Import](https://github.com/boboldehampsink/import) plugin, which this plugin is clearly influenced by, and [Clearbold](https://github.com/clearbold) for [Craft Import](https://github.com/clearbold/craftimport), along with all the great users who have helped provide feedback, testing and bug reports. +[Bob Olde Hampsink](https://github.com/boboldehampsink) and his amazing work on the [Import](https://github.com/boboldehampsink/import) plugin, [Clearbold](https://github.com/clearbold) for [Craft Import](https://github.com/clearbold/craftimport), along with all the great users who have helped provide feedback, testing and bug reports. [Pixel & Tonic](https://github.com/pixelandtonic) for their amazing support, assistance, and of course for creating Craft. -## Changelog +### Changelog [View JSON Changelog](https://github.com/engram-design/FeedMe/blob/master/changelog.json) diff --git a/changelog.json b/changelog.json index a667ca02..af797abb 100644 --- a/changelog.json +++ b/changelog.json @@ -1,27 +1,77 @@ [ { - "version": "1.4.12", - "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.12.zip", - "date": "2016-07-06T01:00:00+10:00", + "version": "2.0.0", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/2.0.0.zip", + "date": "2016-11-01 00:00:00", "notes": [ - "[Improved] Altered terminology around Duplication Handling to hopefully be more clearer.", - "[Fixed] Protect against errors on Feed Me index page when sections/entry types no longer exist.", - "[Fixed] Fixed mapping issues with Table field inside Matrix blocks.", - "[Fixed] Fixed Dropdown field matching via Label, not Value." - ] - }, - { - "version": "1.4.11", - "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.11.zip", - "date": "2016-04-25T19:30:00+10:00", - "notes": [ - "[Fixed] Fixed issue with XML parsing and special characters encoding incorrectly." + "# Major features", + "[Added] Added support for Categories, Users, Entries, Commerce Products", + "[Added] Support for third-party element types", + "[Added] Auto-upload Assets when mapping", + "[Added] Support to map content to element's inner fields (think fields for assets)", + "[Improved] Matrix smart-checking for existing content", + "[Improved] New parsing method for XML feeds, includes attribute-mapping", + "[Improved] New mapping interface allows setting defaults for most fields", + + "# Feeds", + "[Added] Direct access to mapping screen", + "[Added] Support attribute-mapping for XML feeds", + "[Added] Added Debug controller action to help debug those tricky feeds, and see whats going on", + "[Added] Support for third-party data-types, in addition to the native JSON/XML processing. Useful for custom data handling", + "[Improved] Support for save shortcut, and stays on the same screen, rather than redirecting back to index", + "[Improved] Better feedback screen when unable to parse/find feed", + "[Improved] Feed mapping now looks at entire feed structure for nodes, rather than just the first node", + "[Improved] Feed mapping is no longer case-insensitive", + "[Improved] Proper confirmation screen when importing, with progress bar", + "[Fixed] Remove database logging (no longer used)", + "[Fixed] Fix support for local feeds", + "[Fixed] Feed no longer lags when processing from the control panel", + "[Fixed] Fix issue where task wouldn't fire asynchronously, locking up the CP", + "[Fixed] Fixed issue where pending/disabled existing entries weren't being matched for updating/deleting", + "[Fixed] Prevent feed from processing if there are no nodes to process. Fixes deletion when elements shouldn't be", + + "# Fieldtypes", + "[Added] Elements can be created if they don't exist", + "[Added] Assets can be uploaded automatically, with options for conflict handling", + "[Added] Added support to map element fields' own custom fields (think fields for assets). Currently only supports simple fields.", + "[Improved] More modular handling by moving to separate classes", + "[Improved] More streamlined third-party integration and implementation using `registerFeedMeFieldTypes`", + "[Improved] Improved performance for Element fields - replaces `search` with attributes (ie: `asset->filename` over `asset->search`).", + "[Improved] Matrix fields now smartly look at existing content and update only if data has changed. No more element bloat.", + + "# Elements", + "[Added] Support for importing Categories, Users, Entries, Commerce Products", + "[Added] Support for third-party Element Types using `registerFeedMeElementTypes`", + + "# Events", + "[Added] Added `onBeforeProcessFeed`, `onProcessFeed`, and `onStepProcessFeed` events", + + "# Developers", + "[Added] Added `registerFeedMeDataTypes`, `registerFeedMeElementTypes`, and `registerFeedMeFieldTypes` hooks", + + "# Logs", + "[Added] Added ability to clear logs", + "[Improved] Improved logging information across the plugin", + + "# Help", + "[Improved] Better feedback when help requests fail", + "[Fixed] Fixes to Help requests not validating - therefore unable to send", + "[Fixed] Help request form supports CSRF protection - as it should", + + "# Settings", + "[Added] Added ability to clear pending tasks - can be called via Cron", + + "# Documentation", + "[Added] Dedicated plugin page via [http://sgroup.com.au/plugins/feedme](http://sgroup.com.au/plugins/feedme)", + "[Added] Start to finish examples", + "[Added] Examples for JSON/RSS/XML feeds", + "[Added] Developer resources for hooks and events" ] }, { "version": "1.4.10", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.10.zip", - "date": "2016-04-14T06:30:00+10:00", + "date": "2016-04-14 06:30:00", "notes": [ "[Fixed] Fixed issue for repeatable fields containing empty values (table fields).", "[Improved] FeedUrl attribute stored as `TEXT` column type to allow for longer URLs.", @@ -31,7 +81,7 @@ { "version": "1.4.9", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.9.zip", - "date": "2016-03-15T22:25:00+10:00", + "date": "2016-03-15 22:25:00", "notes": [ "[Fixed] Fixed issue with utf8 encoding for feeds.", "[Improved] Improvements to matrix processing." @@ -40,7 +90,7 @@ { "version": "1.4.8", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.8.zip", - "date": "2016-03-03T10:00:00+10:00", + "date": "2016-03-03 10:00:00", "notes": [ "[Added] Fix for json parsing when special characters in feed content.", "[Improved] Better logging when a feed cannot be parsed." @@ -49,7 +99,7 @@ { "version": "1.4.7", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.7.zip", - "date": "2016-02-29T01:00:00+10:00", + "date": "2016-02-29 01:00:00", "notes": [ "[Added] Added support for locales - set which locale you want your feed to go to.", "[Added] Added support for non-http protocols for feeds (ftp://, file://, etc) [#29](https://github.com/engram-design/FeedMe/issues/29)" @@ -58,7 +108,7 @@ { "version": "1.4.6", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.6.zip", - "date": "2016-01-20T13:00:00+10:00", + "date": "2016-01-20 13:00:00", "notes": [ "[Fixed] Fixed an issue where an error would be incorrectly thrown when Add duplication handling is used." ] @@ -66,7 +116,7 @@ { "version": "1.4.5", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.5.zip", - "date": "2016-01-13T18:15:00+10:00", + "date": "2016-01-13 18:15:00", "notes": [ "[Fixed] Fixed issue with plugin release feed url." ] @@ -74,7 +124,7 @@ { "version": "1.4.4", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.4.zip", - "date": "2015-12-28T13:10:00+10:00", + "date": "2015-12-28 13:10:00", "notes": [ "[Fixed] Fixed issue with irregular nested elements. See [#24](https://github.com/engram-design/FeedMe/issues/24#issuecomment-167106972)." ] @@ -82,7 +132,7 @@ { "version": "1.4.3", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.3.zip", - "date": "2015-12-01T19:35:00+10:00", + "date": "2015-12-01 19:35:00", "notes": [ "[Fixed] Check for both numeric and string single-string arrays. Particularly an issue for JSON feeds." ] @@ -90,7 +140,7 @@ { "version": "1.4.2", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.2.zip", - "date": "2015-11-28T00:35:00+10:00", + "date": "2015-11-28 00:35:00", "notes": [ "[Improved] Minor improvements for plugin icons." ] @@ -98,7 +148,7 @@ { "version": "1.4.1", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.1.zip", - "date": "2015-11-27T09:45:00+10:00", + "date": "2015-11-27 09:45:00", "notes": [ "[Fixed] Fix css/js resources filename, which did not commit properly." ] @@ -106,7 +156,7 @@ { "version": "1.4.0", "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.4.0.zip", - "date": "2015-11-26T00:00:00+10:00", + "date": "2015-11-26 00:00:00", "notes": [ "[Added] Craft 2.5 support, including release feed and icons.", "[Improved] Code cleanup and refactoring field-mapping logic for performance and sanity.", @@ -124,5 +174,194 @@ "[Added] Added documentation for hooks. Refer to [Wiki](https://github.com/engram-design/FeedMe/wiki/Hooks).", "[Improved] Modified third-party hooks `prepForFeedMeFieldType` so it actually works! Thanks go to [@lindseydiloreto](https://github.com/lindseydiloreto)." ] + }, + { + "version": "1.3.6", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.3.6.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Removed `file_get_contents` as default method of fetching feed data in favour of Curl.", + "Better error logging when trying to consume feed data.", + "Fix for when mapping to Matrix field, commas were escaping content into new blocks.", + "Ensure fields within Matrix and SuperTable are parsed through necessary field processing functions.", + "Added `prepForFeedMeFieldType` hook for other plugins to handle their own fields." + ] + }, + { + "version": "1.3.5", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.3.5.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Minor fix for logging. When Delete duplication option was set, import success was never recorded in the logs." + ] + }, + { + "version": "1.3.4", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.3.4.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Minor fix for template mapping. Caused an issue when using a JSON feed and came across a empty nested array." + ] + }, + { + "version": "1.3.3", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.3.3.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Minor fix for log reporting which wasn't displaying errors in a useful way." + ] + }, + { + "version": "1.3.2", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.3.2.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Alterations to logging information to provide better feedback. Thanks to [@russback](https://github.com/russback)." + ] + }, + { + "version": "1.3.1", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.3.1.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Fix for info/notice log messages not saving when `devMode` is off." + ] + }, + { + "version": "1.3.0", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.3.0.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Refactored direct processing to utalize Craft's tasks service, rather than using pure PHP processing. This greatly improves performance as PHP processing would run out of memory extremely quickly." + ] + }, + { + "version": "1.2.9", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.9.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Added support for [SuperTable](https://github.com/engram-design/SuperTable).", + "Added log tab to read in `craft/storage/runtime/logs/feedme.log`.", + "Added help tab, allowing users to submit their feed info and setup for debugging/troubleshooting.", + "Fix for fields in Matrix blocks only succesfully mapping textual fields. Complex fields such as Assets, Entries, etc were not mapping correctly.", + "Fix for only one item being processed when Delete duplication handling was selected.", + "Fix for Dropdown/RadioButtons causing a critical error when a provided value didn't exist in the field.", + "Added credit and plugin url to footer." + ] + }, + { + "version": "1.2.8", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.8.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Move changelog - so much change!", + "Add support for attribute values for XML feeds (template tags only).", + "Add missing log statement for successful update/add." + ] + }, + { + "version": "1.2.7", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.7.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Fix where entries would not import if mapping element fields had more values that their field limit.", + "Fix for multiple matches found on existing categories, where only one should match.", + "Fix for escaping special characters in tags/category name.", + "Minor fix for tags/category mapping." + ] + }, + { + "version": "1.2.6", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.6.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Fix for matching fields containing special characters.", + "Fix for tags and category mapping, mapping all available if supplied empty value.", + "Fix for backup lightswitch reflecting the saved state.", + "Fix to ensure at least one duplicate field is checked." + ] + }, + { + "version": "1.2.5", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.5.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Refactoring for performance improvements.", + "Remove database logging until a better performing option is figured out. Logging still occurs to the file system under `craft/storage/runtime/logs/`.", + "Added optional backup option per-feed (default to true).", + "Minor fix so direct feed link doesn't use `siteUrl`." + ] + }, + { + "version": "1.2.4", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.4.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Added support to fallback on cURL when `file_get_contents()` is not available. Can occur on some hosts where `allow_url_fopen = 0`." + ] + }, + { + "version": "1.2.3", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.3.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Primary Element is no longer required - important for JSON feeds.", + "Fixes for when no primary element specified. It's pretty much optional now.", + "UI tidy for mapping table.", + "Fix for duplication handling not matching in some cases. Now uses element search." + ] + }, + { + "version": "1.2.2", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.2.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "JSON feed support." + ] + }, + { + "version": "1.2.1", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.1.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Matrix support.", + "Table support.", + "Even better element-search.", + "Remove square brackets for nested field - serialization issues. **Breaking change** you will need to re-map some fields due to this fix.", + "Fix for supporting multiple entry types when selecting fields to map." + ] + }, + { + "version": "1.2", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.2.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Lots of fixes and improvements for direct-processing. Includes URL parameter, passkey and non-Task processing.", + "Fixes with logging - now more informative!", + "Improvement nested element parsing.", + "Better date parsing.", + "CSRF protection compatibility.", + "Fix for duplicate field mapping not being remembered." + ] + }, + { + "version": "1.1", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.1.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Prep for Table/Matrix mapping.", + "Better depth-mapping for feed data (was limited to depth-2).", + "Refactor field-mapping processing.", + "Set minimum Craft build." + ] + }, + { + "version": "1.0", + "downloadUrl": "https://github.com/engram-design/FeedMe/archive/1.0.zip", + "date": "2015-11-26 00:00:00", + "notes": [ + "Initial release." + ] } ] + diff --git a/feedme/FeedMe/DataTypes/BaseFeedMeDataType.php b/feedme/FeedMe/DataTypes/BaseFeedMeDataType.php new file mode 100644 index 00000000..e4b79ab2 --- /dev/null +++ b/feedme/FeedMe/DataTypes/BaseFeedMeDataType.php @@ -0,0 +1,20 @@ +feedMe_data->getRawData($url))) { + FeedMePlugin::log($settings->name . ': Unable to reach ' . $url . '. Check this is the correct URL.', LogLevel::Error, true); + + return false; + } + + // Parse the JSON string - using Yii's built-in cleanup + try { + $json_array = JsonHelper::decode($raw_content, true); + } catch (Exception $e) { + FeedMePlugin::log($settings->name . ': Invalid JSON - ' . $e->getMessage(), LogLevel::Error, true); + + return false; + } + + // Look for and return only the items for primary element + if ($primaryElement && is_array($json_array)) { + $json_array = craft()->feedMe_data->findPrimaryElement($primaryElement, $json_array); + } + + if (!is_array($json_array)) { + FeedMePlugin::log($settings->name . ': Invalid JSON. - ' . json_last_error_msg(), LogLevel::Error, true); + + return false; + } + + return $json_array; + } +} diff --git a/feedme/FeedMe/DataTypes/XmlFeedMeDataType.php b/feedme/FeedMe/DataTypes/XmlFeedMeDataType.php new file mode 100644 index 00000000..23a753c1 --- /dev/null +++ b/feedme/FeedMe/DataTypes/XmlFeedMeDataType.php @@ -0,0 +1,43 @@ +feedMe_data->getRawData($url))) { + FeedMePlugin::log($settings->name . ': Unable to reach ' . $url . '. Check this is the correct URL.', LogLevel::Error, true); + + return false; + } + + // Parse the XML string into an array + try { + $xml_array = Xml::build($raw_content); + $xml_array = Xml::toArray($xml_array); + } catch (Exception $e) { + FeedMePlugin::log($settings->name . ': Invalid XML - ' . $e->getMessage(), LogLevel::Error, true); + + return false; + } + + // Look for and return only the items for primary element + if ($primaryElement && is_array($xml_array)) { + $xml_array = craft()->feedMe_data->findPrimaryElement($primaryElement, $xml_array); + } + + if (!is_array($xml_array)) { + FeedMePlugin::log($settings->name . ': Invalid XML - ' . print_r($xml_array, true), LogLevel::Error, true); + + return false; + } + + return $xml_array; + } + +} diff --git a/feedme/FeedMe/ElementTypes/AssetFeedMeElementType.php b/feedme/FeedMe/ElementTypes/AssetFeedMeElementType.php new file mode 100644 index 00000000..84beb047 --- /dev/null +++ b/feedme/FeedMe/ElementTypes/AssetFeedMeElementType.php @@ -0,0 +1,126 @@ +assetSources->getAllSources(); + } + + public function setModel($settings) + { + // Set up new asset model + $element = new AssetFileModel(); + $element->sourceId = $settings['elementGroup']['Asset']; + + if ($settings['locale']) { + $element->locale = $settings['locale']; + } + + return $element; + } + + public function setCriteria($settings) + { + // Match with current data + $criteria = craft()->elements->getCriteria(ElementType::Asset); + $criteria->status = null; + $criteria->limit = null; + $criteria->localeEnabled = null; + + $criteria->groupId = $settings['elementGroup']['Asset']; + + return $criteria; + } + + public function matchExistingElement(&$criteria, $data, $settings) + { + foreach ($settings['fieldUnique'] as $handle => $value) { + if ((int)$value === 1) { + $feedValue = Hash::get($data, $handle . '.data', $data[$handle]); + + if ($feedValue) { + $criteria->$handle = DbHelper::escapeParam($feedValue); + } + } + } + + // Check to see if an element already exists - interestingly, find()[0] is faster than first() + return $criteria->find(); + } + + public function delete(array $elements) + { + return craft()->assets->deleteFiles($elements); + } + + public function prepForElementModel(BaseElementModel $element, array &$data, $settings) + { + foreach ($data as $handle => $value) { + switch ($handle) { + case 'id'; + $element->$handle = $value['data']; + break; + case 'filename'; + $element->$handle = $value; + break; + case 'title': + $element->getContent()->$handle = $value['data']; + break; + default: + continue 2; + } + + // Update the original data in our feed - for clarity in debugging + $data[$handle] = $element->$handle; + } + + return $element; + } + + public function save(BaseElementModel &$element, array $data, $settings) + { + // Prep some variables + $fieldData = $data[array_keys($data)[0]]; + + // Check if we're dealing with uplading assets + if (isset($fieldData['options']['upload'])) { + $service = craft()->feedMe->getFieldTypeService('Assets'); + $folder = craft()->assets->getRootFolderBySourceId($element->sourceId); + $urlData = $fieldData['data']; + + $fileIds = $service->fetchRemoteImage($urlData, $folder->id, $fieldData['options']); + } else { + // There's no real case at the moment if we're not uploading. Why? + // Because theu're already in Craft. Leave for the moment + } + + return true; + } + + public function afterSave(BaseElementModel $element, array $data, $settings) + { + + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/ElementTypes/BaseFeedMeElementType.php b/feedme/FeedMe/ElementTypes/BaseFeedMeElementType.php new file mode 100644 index 00000000..0cf60a79 --- /dev/null +++ b/feedme/FeedMe/ElementTypes/BaseFeedMeElementType.php @@ -0,0 +1,38 @@ +categories->getEditableGroups(); + } + + public function setModel($settings) + { + // Set up new category model + $element = new CategoryModel(); + $element->groupId = $settings['elementGroup']['Category']; + + if ($settings['locale']) { + $element->locale = $settings['locale']; + } + + return $element; + } + + public function setCriteria($settings) + { + // Match with current data + $criteria = craft()->elements->getCriteria(ElementType::Category); + $criteria->status = null; + $criteria->limit = null; + $criteria->localeEnabled = null; + + $criteria->groupId = $settings['elementGroup']['Category']; + + return $criteria; + } + + public function matchExistingElement(&$criteria, $data, $settings) + { + foreach ($settings['fieldUnique'] as $handle => $value) { + if ((int)$value === 1) { + $feedValue = Hash::get($data, $handle . '.data', $data[$handle]); + + if ($feedValue) { + $criteria->$handle = DbHelper::escapeParam($feedValue); + } + } + } + + // Check to see if an element already exists - interestingly, find()[0] is faster than first() + return $criteria->find(); + } + + public function delete(array $elements) + { + return craft()->categories->deleteCategory($elements); + } + + public function prepForElementModel(BaseElementModel $element, array &$data, $settings) + { + foreach ($data as $handle => $value) { + switch ($handle) { + case 'id'; + $element->$handle = $value['data']; + break; + case 'slug': + $element->$handle = ElementHelper::createSlug($value['data']); + break; + //case 'parent': + //$element->parent = $this->_findParent($value); + //break; + case 'title': + $element->getContent()->$handle = $value['data']; + break; + default: + continue 2; + } + + // Update the original data in our feed - for clarity in debugging + $data[$handle] = $element->$handle; + } + + return $element; + } + + public function save(BaseElementModel &$element, array $data, $settings) + { + // Are we targeting a specific locale here? If so, we create an essentially blank element + // for the primary locale, and instead create a locale for the targeted locale + if (isset($settings['locale'])) { + // Save the default locale element empty + if (craft()->categories->saveCategory($element)) { + // Now get the successfully saved (empty) element, and set content on that instead + $elementLocale = craft()->categories->getCategoryById($element->id, $settings['locale']); + $elementLocale->setContentFromPost($data); + + // Save the locale entry + return craft()->categories->saveCategory($elementLocale); + } else { + if ($element->getErrors()) { + throw new Exception(json_encode($element->getErrors())); + } else { + throw new Exception(Craft::t('Unknown Element error occurred.')); + } + } + + return false; + } else { + return craft()->categories->saveCategory($element); + } + } + + public function afterSave(BaseElementModel $element, array $data, $settings) + { + $parentCategory = null; + + if (isset($data['parent'])) { + $parentCategory = $this->_prepareParentForElement($data['parent'], $element->groupId); + } + + if ($parentCategory) { + $categoryGroup = craft()->categories->getGroupById($element->groupId); + craft()->structures->append($categoryGroup->structureId, $element, $parentCategory, 'auto'); + } + } + + + + // Private Methods + // ========================================================================= + + private function _prepareParentForElement($fieldData, $groupId) + { + $parentCategory = null; + + $data = Hash::get($fieldData, 'data'); + $attribute = Hash::get($fieldData, 'options.match', 'id'); + + if (!empty($data)) { + $criteria = craft()->elements->getCriteria(ElementType::Category); + $criteria->groupId = $groupId; + $criteria->$attribute = DbHelper::escapeParam($data); + $criteria->limit = 1; + $parentCategory = $criteria->first(); + } + + return $parentCategory; + } +} diff --git a/feedme/FeedMe/ElementTypes/Commerce_ProductFeedMeElementType.php b/feedme/FeedMe/ElementTypes/Commerce_ProductFeedMeElementType.php new file mode 100644 index 00000000..fa97732b --- /dev/null +++ b/feedme/FeedMe/ElementTypes/Commerce_ProductFeedMeElementType.php @@ -0,0 +1,345 @@ +commerce_productTypes->getEditableProductTypes(); + } + + public function setModel($settings) + { + $element = new Commerce_ProductModel(); + $element->typeId = $settings['elementGroup']['Commerce_Product']; + + if ($settings['locale']) { + $element->locale = $settings['locale']; + } + + return $element; + } + + public function setCriteria($settings) + { + $criteria = craft()->elements->getCriteria('Commerce_Product'); + $criteria->status = null; + $criteria->limit = null; + $criteria->localeEnabled = null; + + $criteria->typeId = $settings['elementGroup']['Commerce_Product']; + + if ($settings['locale']) { + $criteria->locale = $settings['locale']; + } + + return $criteria; + } + + public function matchExistingElement(&$criteria, $data, $settings) + { + foreach ($settings['fieldUnique'] as $handle => $value) { + if (intval($value) == 1 && ($data != '__')) { + if (strstr($handle, 'variants--')) { + $attribute = str_replace('variants--', '', $handle); + + // If we're matching existing elements via a Variant property, we don't want to use the + // Commerce_Product element criteria + $variantCriteria = craft()->elements->getCriteria('Commerce_Variant'); + $variantCriteria->status = null; + $variantCriteria->limit = null; + $variantCriteria->localeEnabled = null; + + $variantCriteria->$attribute = DbHelper::escapeParam($data['variants']['data'][$attribute]['data']); + + // Get the variants - interestingly, find()[0] is faster than first() + $variants = $variantCriteria->find(); + + // Set the Product ID for the criteria from our found variant - thats what we need to update + if (isset($variants[0])) { + $criteria->id = $variants[0]->productId; + } + } else { + $criteria->$handle = DbHelper::escapeParam($data[$handle]); + } + } + } + + // Check to see if an element already exists - interestingly, find()[0] is faster than first() + return $criteria->find(); + } + + public function delete(array $elements) + { + $return = true; + + foreach ($elements as $element) { + if (!craft()->commerce_products->deleteProduct($element)) { + $return = false; + } + } + + return $return; + } + + public function prepForElementModel(BaseElementModel $element, array &$data, $settings) + { + foreach ($data as $handle => $value) { + switch ($handle) { + case 'id'; + case 'taxCategoryId'; + case 'shippingCategoryId'; + $element->$handle = $value['data']; + break; + case 'slug'; + $element->$handle = ElementHelper::createSlug($value['data']); + break; + case 'postDate': + case 'expiryDate'; + $element->$handle = $this->_prepareDateForElement($value['data']); + break; + case 'enabled': + case 'freeShipping': + case 'promotable': + $element->$handle = (bool)$value['data']; + break; + case 'title': + $element->getContent()->$handle = $value['data']; + break; + default: + continue 2; + } + + // Update the original data in our feed - for clarity in debugging + $data[$handle] = $element->$handle; + } + + $this->_populateProductVariantModels($element, $data, $settings); + + return $element; + } + + public function save(BaseElementModel &$element, array $data, $settings) + { + $result = craft()->commerce_products->saveProduct($element); + + // Because we can have product and variant error, make sure we show them + if (!$result) { + foreach ($element->getVariants() as $variant) { + if ($variant->getErrors()) { + throw new Exception(json_encode($variant->getErrors())); + } + } + } + + return $result; + } + + public function afterSave(BaseElementModel $element, array $data, $settings) + { + + } + + + // Private Methods + // ========================================================================= + + private function _populateProductModel(Commerce_ProductModel &$product, $data) + { + + } + + private function _populateProductVariantModels(Commerce_ProductModel $product, &$data, $settings) + { + $variants = []; + $count = 1; + + $variantData = Hash::get($data, 'variants.data'); + + if (!$variantData) { + return false; + } + + $variantData = $this->_prepProductData($variantData); + + // Update original data + $data['variants'] = $variantData; + + foreach ($variantData as $key => $variant) { + $variantModel = $this->_getVariantBySku($variant['sku']['data']); + + if (!$variantModel) { + $variantModel = new Commerce_VariantModel(); + } + + $variantModel->setProduct($product); + + // Check for our default data (if any provided, and if not already set in 'real' data) + foreach ($settings['fieldDefaults'] as $defaultsHandle => $defaultsValue) { + if ($defaultsValue) { + $variantPreppedHandle = str_replace('variants--', '', $defaultsHandle); + + $variant[$variantPreppedHandle]['data'] = $defaultsValue; + } + } + + $variantModel->enabled = Hash::get($variant, 'enabled.data', 1); + $variantModel->isDefault = Hash::get($variant, 'isDefault.data', 0); + $variantModel->sku = Hash::get($variant, 'sku.data'); + $variantModel->price = Hash::get($variant, 'price.data'); + $variantModel->width = LocalizationHelper::normalizeNumber(Hash::get($variant, 'width.data')); + $variantModel->height = LocalizationHelper::normalizeNumber(Hash::get($variant, 'height.data')); + $variantModel->length = LocalizationHelper::normalizeNumber(Hash::get($variant, 'length.data')); + $variantModel->weight = LocalizationHelper::normalizeNumber(Hash::get($variant, 'weight.data')); + $variantModel->stock = LocalizationHelper::normalizeNumber(Hash::get($variant, 'stock.data')); + $variantModel->unlimitedStock = LocalizationHelper::normalizeNumber(Hash::get($variant, 'unlimitedStock.data')); + $variantModel->minQty = LocalizationHelper::normalizeNumber(Hash::get($variant, 'minQty.data')); + $variantModel->maxQty = LocalizationHelper::normalizeNumber(Hash::get($variant, 'maxQty.data')); + + $variantModel->sortOrder = $count++; + + // Loop through each field for this Variant model - see if we have data + $variantContent = array(); + foreach ($variantModel->getFieldLayout()->getFields() as $fieldLayout) { + $field = $fieldLayout->getField(); + $handle = $field->handle; + + $fieldData = Hash::get($variant, $handle); + + if ($fieldData) { + $variantContent[$handle] = craft()->feedMe_fields->prepForFieldType($variantModel, $fieldData, $handle); + } + } + + $variantModel->setContentFromPost($variantContent); + + $variantModel->getContent()->title = Hash::get($variant, 'title.data'); + + $variants[] = $variantModel; + } + + $product->setVariants($variants); + } + + private function _prepProductData($variantData) { + $variants = array(); + + // Check for single Variant - thats easy + if (Hash::dimensions($variantData) == 2) { + return array($variantData); + } + + // We need to parse our variant data, because they're stored in a specific way from field-mapping + // [title] => Array ( + // [data] => Array ( + // [0] => Product 1 + // [1] => Product 2 + // ) + // Into: + // [0] => Array ( + // [data] => Array ( + // [title] => Product 1 + // ) + // [1] => Array ( + // [data] => Array ( + // [title] => Product 2 + // ) + + $flatten = Hash::flatten($variantData); + + $optionsArray = array(); + $tempVariants = array(); + foreach ($flatten as $keyedIndex => $value) { + $tempArray = explode('.', $keyedIndex); + + // Check for a value for this field... + if (!isset($value) || $value === null) { + continue; + } + + if (is_array($value) && empty($value)) { + continue; + } + + // Save field options for later - they're a special case + if (strstr($keyedIndex, '.options.')) { + FeedMeArrayHelper::arraySet($optionsArray, $tempArray, $value); + } else { + // Extract 'data.[number]' - we need the number for which variant we're talking about + preg_match_all('/data.(\d*)/', $keyedIndex, $variantKeys); + $fieldHandle = $tempArray[0]; + $variantKey = $variantKeys[1]; + + // Remove the index from inside [data], to the front + array_splice($tempArray, 0, 0, $variantKey); + + // Check for nested data (elements, table) + if (preg_match('/data.(\d*\.\d*)/', $keyedIndex)) { + //array_pop($tempArray); + + unset($tempArray[count($tempArray) - 2]); + } else { + array_pop($tempArray); + } + + // Special case for Table field. This will be refactored once again with field-aware-parsing + $field = craft()->fields->getFieldByHandle($fieldHandle); + + if ($field && $field->type == 'Table') { + array_splice($tempArray, 2, 0, 'data'); + } + + FeedMeArrayHelper::arraySet($variants, $tempArray, $value); + } + } + + // Put the variants back in place where they should be + foreach ($variants as $blockOrder => $blockData) { + foreach ($blockData as $blockHandle => $innerData) { + $optionData = Hash::get($optionsArray, $blockHandle); + + if ($optionData) { + $variants[$blockOrder][$blockHandle] = Hash::merge($innerData, $optionData); + } + } + } + + return $variants; + } + + private function _getVariantBySku($sku, $localeId = null) + { + return craft()->elements->getCriteria('Commerce_Variant', array('sku' => $sku, 'status' => null, 'locale' => $localeId))->first(); + } + + private function _prepareDateForElement($date) + { + if (!is_array($date)) { + $d = date_parse($date); + $date_string = date('Y-m-d H:i:s', mktime($d['hour'], $d['minute'], $d['second'], $d['month'], $d['day'], $d['year'])); + + $date = DateTime::createFromString($date_string, craft()->timezone); + } + + return $date; + } +} diff --git a/feedme/FeedMe/ElementTypes/EntryFeedMeElementType.php b/feedme/FeedMe/ElementTypes/EntryFeedMeElementType.php new file mode 100644 index 00000000..9347828b --- /dev/null +++ b/feedme/FeedMe/ElementTypes/EntryFeedMeElementType.php @@ -0,0 +1,253 @@ +sections->getEditableSections(); + + // Get sections but not singles + $sections = array(); + foreach ($editable as $section) { + if ($section->type != SectionType::Single) { + $sections[] = $section; + } + } + + return $sections; + } + + public function setModel($settings) + { + $element = new EntryModel(); + $element->sectionId = $settings['elementGroup']['Entry']['section']; + $element->typeId = $settings['elementGroup']['Entry']['entryType']; + + if ($settings['locale']) { + $element->locale = $settings['locale']; + } + + return $element; + } + + public function setCriteria($settings) + { + $criteria = craft()->elements->getCriteria(ElementType::Entry); + $criteria->status = null; + $criteria->limit = null; + $criteria->localeEnabled = null; + + $criteria->sectionId = $settings['elementGroup']['Entry']['section']; + $criteria->type = $settings['elementGroup']['Entry']['entryType']; + + if ($settings['locale']) { + $criteria->locale = $settings['locale']; + } + + return $criteria; + } + + public function matchExistingElement(&$criteria, $data, $settings) + { + foreach ($settings['fieldUnique'] as $handle => $value) { + if ((int)$value === 1) { + $feedValue = Hash::get($data, $handle . '.data', $data[$handle]); + + // Special-case for Title which can be dynamic + if ($handle == 'title') { + $entryTypeId = $settings['elementGroup']['Entry']['entryType']; + $entryType = craft()->sections->getEntryTypeById($entryTypeId); + + // Its dynamically generated + if (!$entryType->hasTitleField) { + $feedValue = craft()->templates->renderObjectTemplate($entryType->titleFormat, $data); + } + } + + if ($feedValue) { + $criteria->$handle = DbHelper::escapeParam($feedValue); + } + } + } + + // Check to see if an element already exists - interestingly, find()[0] is faster than first() + return $criteria->find(); + } + + public function delete(array $elements) + { + return craft()->entries->deleteEntry($elements); + } + + public function prepForElementModel(BaseElementModel $element, array &$data, $settings) + { + $checkAncestors = !isset($data['parentId']); + + foreach ($data as $handle => $value) { + if (is_null($value)) { + continue; + } + + if (isset($value['data']) && $value['data'] === null) { + continue; + } + + switch ($handle) { + case 'id'; + $element->$handle = $value['data']; + break; + case 'authorId'; + $element->$handle = $this->_prepareAuthorForElement($value['data']); + break; + case 'slug'; + $element->$handle = ElementHelper::createSlug($value['data']); + break; + case 'postDate': + case 'expiryDate'; + $dateValue = $this->_prepareDateForElement($value['data']); + + // Ensure there's a parsed data - null will auto-generate a new date + if ($dateValue) { + $element->$handle = $dateValue; + } + + break; + case 'enabled': + $element->$handle = (bool)$value['data']; + break; + case 'title': + $element->getContent()->$handle = $value['data']; + break; + case 'parent': + $element->parentId = $this->_prepareParentForElement($value, $element->sectionId); + break; + default: + continue 2; + } + + // Update the original data in our feed - for clarity in debugging + $data[$handle] = $element->$handle; + } + + // Set default author if not set + if (!$element->authorId) { + $user = craft()->userSession->getUser(); + $element->authorId = ($element->authorId ? $element->authorId : ($user ? $user->id : 1)); + + // Update the original data in our feed - for clarity in debugging + $data['authorId'] = $element->authorId; + } + + return $element; + } + + public function save(BaseElementModel &$element, array $data, $settings) + { + // Are we targeting a specific locale here? If so, we create an essentially blank element + // for the primary locale, and instead create a locale for the targeted locale + if (isset($settings['locale'])) { + // Save the default locale element empty + if (craft()->entries->saveEntry($element)) { + // Now get the successfully saved (empty) element, and set content on that instead + $elementLocale = craft()->entries->getEntryById($element->id, $settings['locale']); + $elementLocale->setContentFromPost($data); + + // Save the locale entry + return craft()->entries->saveEntry($elementLocale); + } else { + if ($element->getErrors()) { + throw new Exception(json_encode($element->getErrors())); + } else { + throw new Exception(Craft::t('Unknown Element error occurred.')); + } + } + + return false; + } else { + return craft()->entries->saveEntry($element); + } + } + + public function afterSave(BaseElementModel $element, array $data, $settings) + { + + } + + + // Private Methods + // ========================================================================= + + private function _prepareDateForElement($date) + { + $craftDate = null; + + if (!is_array($date)) { + $d = date_parse($date); + $date_string = date('Y-m-d H:i:s', mktime($d['hour'], $d['minute'], $d['second'], $d['month'], $d['day'], $d['year'])); + + $craftDate = DateTime::createFromString($date_string, craft()->timezone); + } + + return $craftDate; + } + + private function _prepareAuthorForElement($author) + { + if (!is_numeric($author)) { + $criteria = craft()->elements->getCriteria(ElementType::User); + $criteria->search = $author; + $authorUser = $criteria->first(); + + if ($authorUser) { + $author = $authorUser->id; + } else { + $user = craft()->users->getUserByUsernameOrEmail($author); + $author = $user ? $user->id : 1; + } + } + + return $author; + } + + private function _prepareParentForElement($fieldData, $sectionId) + { + $parentId = null; + + $data = Hash::get($fieldData, 'data'); + $attribute = Hash::get($fieldData, 'options.match', 'id'); + + if (!empty($data)) { + $criteria = craft()->elements->getCriteria(ElementType::Entry); + $criteria->sectionId = $sectionId; + $criteria->$attribute = DbHelper::escapeParam($data); + $criteria->limit = 1; + + if ($criteria->total()) { + $parentId = $criteria->ids()[0]; + } + } + + return $parentId; + } +} \ No newline at end of file diff --git a/feedme/FeedMe/ElementTypes/UserFeedMeElementType.php b/feedme/FeedMe/ElementTypes/UserFeedMeElementType.php new file mode 100644 index 00000000..84ae77cf --- /dev/null +++ b/feedme/FeedMe/ElementTypes/UserFeedMeElementType.php @@ -0,0 +1,177 @@ +getEdition() == Craft::Pro) { + $groups = craft()->userGroups->getAllGroups(); + + $result = count($groups) ? $groups : true; + } + + return $result; + } + + public function setModel($settings) + { + $element = new UserModel(); + + if ($settings['locale']) { + $element->locale = $settings['locale']; + } + + return $element; + } + + public function setCriteria($settings) + { + $criteria = craft()->elements->getCriteria(ElementType::User); + $criteria->status = null; + $criteria->limit = null; + //$criteria->group = null; + $criteria->localeEnabled = null; + + if ($settings['locale']) { + $criteria->locale = $settings['locale']; + } + + return $criteria; + } + + public function matchExistingElement(&$criteria, $data, $settings) + { + foreach ($settings['fieldUnique'] as $handle => $value) { + if ((int)$value === 1) { + $feedValue = Hash::get($data, $handle . '.data', $data[$handle]); + + if ($feedValue) { + $criteria->$handle = DbHelper::escapeParam($feedValue); + } + } + } + + // Check to see if an element already exists - interestingly, find()[0] is faster than first() + return $criteria->find(); + } + + public function delete(array $elements) + { + $return = true; + + // Delete users + foreach ($elements as $element) { + if (!craft()->users->deleteUser($element)) { + $return = false; + } + } + + return $return; + } + + public function prepForElementModel(BaseElementModel $element, array &$data, $settings) + { + foreach ($data as $handle => $value) { + switch ($handle) { + case 'id': + case 'username': + case 'firstName': + case 'lastName': + case 'email': + case 'prefLocale': + case 'newPassword': + case 'photo': + $element->$handle = $value['data']; + break; + case 'status': + $this->_setUserStatus($element, $value['data']); + break; + default: + continue 2; + } + + // Update the original data in our feed - for clarity in debugging + $data[$handle] = $element->$handle; + } + + // Set email as username + if (craft()->config->get('useEmailAsUsername')) { + $element->username = $element->email; + } + + return $element; + } + + public function save(BaseElementModel &$element, array $data, $settings) + { + if (craft()->users->saveUser($element)) { + craft()->userGroups->assignUserToGroups($element->id, $settings['elementGroup']['User']); + return true; + } + + return false; + } + + public function afterSave(BaseElementModel $element, array $data, $settings) + { + + } + + + // Private Methods + // ========================================================================= + + private function _setUserStatus(UserModel $user, $status) + { + switch ($status) { + case 'locked'; + $user->locked = true; + break; + case 'suspended'; + $user->locked = false; + $user->suspended = true; + break; + case 'archived': + $user->locked = false; + $user->suspended = false; + $user->archived = true; + break; + case 'pending': + $user->locked = false; + $user->suspended = false; + $user->archived = false; + $user->pending = true; + break; + case 'active': + $user->suspended = false; + $user->locked = false; + $user->setActive(); + break; + } + + return $user; + } +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/AssetsFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/AssetsFeedMeFieldType.php new file mode 100644 index 00000000..bf30a457 --- /dev/null +++ b/feedme/FeedMe/FieldTypes/AssetsFeedMeFieldType.php @@ -0,0 +1,251 @@ +getFieldType()->getSettings(); + + // Get folder id's for connecting + $folderIds = array(); + $folders = $settings->getAttribute('sources'); + if (is_array($folders)) { + foreach ($folders as $folder) { + list(, $id) = explode(':', $folder); + $folderIds[] = $id; + + // Get all sub-folders for this root folder + $folderModel = craft()->assets->getFolderById($id); + + if ($folderModel) { + $subFolders = craft()->assets->getAllDescendantFolders($folderModel); + + if (is_array($subFolders)) { + foreach ($subFolders as $subFolder) { + $folderIds[] = $subFolder->id; + } + } + } + } + } + + // Find existing asset + foreach ($data as $asset) { + // Check config settings if we need to clean url + if (craft()->config->get('cleanAssetUrls', 'feedMe')) { + $asset = UrlHelper::stripQueryString($asset); + } + + // Cleanup filenames to match Craft Assets + //$asset = AssetsHelper::cleanAssetName($asset); + $asset = str_replace(',', '\,', $asset); + + $criteria = craft()->elements->getCriteria(ElementType::Asset); + $criteria->status = null; + $criteria->folderId = $folderIds; + $criteria->limit = $settings->limit; + $criteria->filename = $asset; + + $preppedData = array_merge($preppedData, $criteria->ids()); + } + + // Check to see if we should be uploading these assets + if (isset($fieldData['options']['upload'])) { + // Get the folder we should upload into from the field + $folderId = $field->getFieldType()->resolveSourcePath(); + + $ids = $this->fetchRemoteImage($data, $folderId, $fieldData['options']); + + $preppedData = array_merge($preppedData, $ids); + } + + // Check for field limit - only return the specified amount + if ($preppedData) { + if ($field->settings['limit']) { + $preppedData = array_chunk($preppedData, $field->settings['limit']); + $preppedData = $preppedData[0]; + } + } + + // Check if we've got any data for the fields in this element + if (isset($fieldData['fields'])) { + $this->_populateElementFields($preppedData, $fieldData['fields']); + } + + return $preppedData; + } + + public function fetchRemoteImage($urls, $folderId, $options) + { + if (!is_array($urls)) { + $urls = array($urls); + } + + $fileIds = array(); + $tempPath = craft()->path->getTempPath(); + + // Check if the temp path exists first + if (!IOHelper::getRealPath($tempPath)) { + IOHelper::createFolder($tempPath, craft()->config->get('defaultFolderPermissions'), true); + + if (!IOHelper::getRealPath($tempPath)) { + throw new Exception(Craft::t('Temp folder “{tempPath}” does not exist and could not be created', array('tempPath' => $tempPath))); + } + } + + // Download each image + foreach ($urls as $key => $url) { + if (!isset($url) || $url === '') { + continue; + } + + // Check config settings if we need to clean url + if (craft()->config->get('cleanAssetUrls', 'feedMe')) { + $url = UrlHelper::stripQueryString($url); + } + + $filename = basename($url); + $saveLocation = $tempPath . $filename; + + // Check if this URL has has a file extension - grab it if not... + $extension = IOHelper::getExtension($saveLocation); + + if (!$extension) { + $image = getimagesize($url); + $extension = FileHelper::getExtensionByMimeType($image['mime']); + + $saveLocation = $saveLocation . '.' . $extension; + $filename = $filename . '.' . $extension; + } + + // Cleanup filenames for Curl specifically + $curlUrl = str_replace(' ', '%20', $url); + + // Download the file - ensuring we're not loading into memory for efficiency + $defaultOptions = array( + CURLOPT_FILE => fopen($saveLocation, 'w'), + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_URL => $curlUrl, + CURLOPT_FAILONERROR => true, + ); + + $configOptions = craft()->config->get('curlOptions', 'feedMe'); + + if ($configOptions) { + $opts = $configOptions + $defaultOptions; + } else { + $opts = $defaultOptions; + } + + $ch = curl_init(); + curl_setopt_array($ch, $opts); + $result = curl_exec($ch); + + if ($result === false) { + //throw new Exception(curl_error($ch)); + FeedMePlugin::log('Asset error: ' . $url . ' - ' . curl_error($ch), LogLevel::Error, true); + continue; + } + + // We've successfully downloaded the image - now insert it into Craft + $conflictResolution = $options['conflict']; + + // Look for an existing file + $criteria = craft()->elements->getCriteria(ElementType::Asset); + $criteria->folderId = $folderId; + $criteria->limit = null; + $criteria->filename = $filename; + $targetFile = $criteria->find(); + + // It seems that even when 'cancel' is set for a conflict resolution, a new element is created + // that seems unnecessary, and could easily cause element bloat... + if ($conflictResolution == AssetConflictResolution::Cancel && isset($targetFile[0])) { + $fileIds[] = $targetFile[0]->id; + } else { + // Wrap in a try/catch to ensure any errors with saving an asset are logged, but don't break the import process + try { + $response = craft()->assets->insertFileByLocalPath($saveLocation, $filename, $folderId, $conflictResolution); + + // Delete temporary file + IOHelper::deleteFile($saveLocation, true); + + if ($response->isSuccess()) { + $fileId = $response->getDataItem('fileId'); + + if ($fileId) { + $fileIds[] = $fileId; + } + } else { + FeedMePlugin::log('Asset error: ' . $url . ' - ' . $response->errorMessage, LogLevel::Error, true); + continue; + } + } catch (Exception $e) { + FeedMePlugin::log('Asset error: ' . $url . ' - ' . $e->getMessage(), LogLevel::Error, true); + } + } + } + + return $fileIds; + } + + + + // Private Methods + // ========================================================================= + + private function _populateElementFields($assetData, $fieldData) + { + foreach ($assetData as $i => $assetId) { + $asset = craft()->assets->getFileById($assetId); + + // Prep each inner field + $preppedData = array(); + foreach ($fieldData as $fieldHandle => $fieldContent) { + $data = craft()->feedMe_fields->prepForFieldType(null, $fieldContent, $fieldHandle, null); + + if (is_array($data)) { + $data = Hash::get($data, $i); + } + + $preppedData[$fieldHandle] = $data; + } + + $asset->setContentFromPost($preppedData); + + if (!craft()->assets->storeFile($asset)) { + FeedMePlugin::log('Asset error: ' . json_encode($asset->getErrors()), LogLevel::Error, true); + } else { + FeedMePlugin::log('Updated Asset (ID ' . $assetId . ') inner-element with content: ' . json_encode($preppedData), LogLevel::Info, true); + } + } + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/BaseFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/BaseFeedMeFieldType.php new file mode 100644 index 00000000..b1f436c0 --- /dev/null +++ b/feedme/FeedMe/FieldTypes/BaseFeedMeFieldType.php @@ -0,0 +1,33 @@ +getFieldType()->getSettings(); + + // Get source id's for connecting + $source = $settings->getAttribute('source'); + list($type, $groupId) = explode(':', $source); + + // Find existing + foreach ($data as $category) { + $criteria = craft()->elements->getCriteria(ElementType::Category); + $criteria->status = null; + $criteria->groupId = $groupId; + $criteria->limit = $settings->limit; + + // Check if we've specified which attribute we're trying to match against + $attribute = Hash::get($fieldData, 'options.match', 'title'); + $criteria->$attribute = DbHelper::escapeParam($category); + $elements = $criteria->ids(); + + $elements = $criteria->ids(); + + $preppedData = array_merge($preppedData, $elements); + + // Create the elements if we require + if (count($elements) == 0) { + if (isset($fieldData['options']['create'])) { + $preppedData[] = $this->_createElement($category, $groupId); + } + } + } + + // Check for field limit - only return the specified amount + if ($preppedData) { + if ($field->settings['limit']) { + $preppedData = array_chunk($preppedData, $field->settings['limit']); + $preppedData = $preppedData[0]; + } + } + + // Check if we've got any data for the fields in this element + if (isset($fieldData['fields'])) { + $this->_populateElementFields($preppedData, $fieldData['fields']); + } + + return $preppedData; + } + + + + // Private Methods + // ========================================================================= + + private function _populateElementFields($categoryData, $fieldData) + { + foreach ($categoryData as $i => $categoryId) { + $category = craft()->categories->getCategoryById($categoryId); + + // Prep each inner field + $preppedData = array(); + foreach ($fieldData as $fieldHandle => $fieldContent) { + $data = craft()->feedMe_fields->prepForFieldType(null, $fieldContent, $fieldHandle, null); + + if (is_array($data)) { + $data = Hash::get($data, $i); + } + + $preppedData[$fieldHandle] = $data; + } + + $category->setContentFromPost($preppedData); + + if (!craft()->categories->saveCategory($category)) { + FeedMePlugin::log('Category error: ' . json_encode($category->getErrors()), LogLevel::Error, true); + } else { + FeedMePlugin::log('Updated Category (ID ' . $categoryId . ') inner-element with content: ' . json_encode($preppedData), LogLevel::Info, true); + } + } + } + + private function _createElement($category, $groupId) + { + $element = new CategoryModel(); + $element->getContent()->title = $category; + $element->groupId = $groupId; + + // Save category + if (craft()->categories->saveCategory($element)) { + return $element->id; + } else { + throw new Exception(json_encode($element->getErrors())); + } + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/CheckboxesFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/CheckboxesFeedMeFieldType.php new file mode 100644 index 00000000..0ea32576 --- /dev/null +++ b/feedme/FeedMe/FieldTypes/CheckboxesFeedMeFieldType.php @@ -0,0 +1,25 @@ +timezone)); + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/DefaultFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/DefaultFeedMeFieldType.php new file mode 100644 index 00000000..1ac3d9cf --- /dev/null +++ b/feedme/FeedMe/FieldTypes/DefaultFeedMeFieldType.php @@ -0,0 +1,28 @@ +getFieldType()->getSettings(); + $options = $settings->getAttribute('options'); + + // find matching option label + foreach ($options as $option) { + if ($data == $option['value']) { + $preppedData = $option['value']; + break; + } + } + + return $preppedData; + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/EntriesFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/EntriesFeedMeFieldType.php new file mode 100644 index 00000000..f527c3e5 --- /dev/null +++ b/feedme/FeedMe/FieldTypes/EntriesFeedMeFieldType.php @@ -0,0 +1,147 @@ +getFieldType()->getSettings(); + + // Get source id's for connecting + $sectionIds = array(); + $sources = $settings->sources; + if (is_array($sources)) { + foreach ($sources as $source) { + // When singles is selected as the only option to search in, it doesn't contain any ids... + if ($source == 'singles') { + foreach (craft()->sections->getAllSections() as $section) { + $sectionIds[] = ($section->type == 'single') ? $section->id : ''; + } + } else { + list($type, $id) = explode(':', $source); + $sectionIds[] = $id; + } + } + } + + // Find existing + foreach ($data as $entry) { + $criteria = craft()->elements->getCriteria(ElementType::Entry); + $criteria->status = null; + $criteria->sectionId = $sectionIds; + $criteria->limit = $settings->limit; + + // Check if we've specified which attribute we're trying to match against + $attribute = Hash::get($fieldData, 'options.match', 'title'); + $criteria->$attribute = DbHelper::escapeParam($entry); + $elements = $criteria->ids(); + + $preppedData = array_merge($preppedData, $elements); + + // Create the elements if we require + if (count($elements) == 0) { + if (isset($fieldData['options']['create'])) { + $preppedData[] = $this->_createElement($entry, $sectionIds, $attribute); + } + } + } + + // Check for field limit - only return the specified amount + if ($preppedData) { + if ($field->settings['limit']) { + $preppedData = array_chunk($preppedData, $field->settings['limit']); + $preppedData = $preppedData[0]; + } + } + + // Check if we've got any data for the fields in this element + if (isset($fieldData['fields'])) { + $this->_populateElementFields($preppedData, $fieldData['fields']); + } + + return $preppedData; + } + + + + // Private Methods + // ========================================================================= + + private function _populateElementFields($entryData, $fieldData) + { + foreach ($entryData as $i => $entryId) { + $entry = craft()->entries->getEntryById($entryId); + + // Prep each inner field + $preppedData = array(); + foreach ($fieldData as $fieldHandle => $fieldContent) { + $data = craft()->feedMe_fields->prepForFieldType(null, $fieldContent, $fieldHandle, null); + + if (is_array($data)) { + $data = Hash::get($data, $i); + } + + $preppedData[$fieldHandle] = $data; + } + + $entry->setContentFromPost($preppedData); + + if (!craft()->entries->saveEntry($entry)) { + FeedMePlugin::log('Entry error: ' . json_encode($entry->getErrors()), LogLevel::Error, true); + } else { + FeedMePlugin::log('Updated Entry (ID ' . $entryId . ') inner-element with content: ' . json_encode($preppedData), LogLevel::Info, true); + } + } + } + + private function _createElement($entry, $sectionIds, $attribute) + { + $fieldSections = array_values(Hash::filter($sectionIds)); + $firstSectionId = $fieldSections[0]; + + $element = new EntryModel(); + + if ($attribute == 'title') { + $element->getContent()->title = DbHelper::escapeParam($entry); + } else { + $element->$attribute = DbHelper::escapeParam($entry); + } + + $element->sectionId = $firstSectionId; + + // Save category + if (craft()->entries->saveEntry($element)) { + return $element->id; + } else { + throw new Exception(json_encode($element->getErrors())); + } + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/LightswitchFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/LightswitchFeedMeFieldType.php new file mode 100644 index 00000000..1b0b19a6 --- /dev/null +++ b/feedme/FeedMe/FieldTypes/LightswitchFeedMeFieldType.php @@ -0,0 +1,29 @@ + $value) { + $tempArray = explode('.', $keyedIndex); + + // Save field options for later - they're a special case + if (strstr($keyedIndex, '.options.')) { + FeedMeArrayHelper::arraySet($optionsArray, $tempArray, $value); + } else { + preg_match_all('/data.(\d*)/', $keyedIndex, $blockKeys); + $blockKey = $blockKeys[1]; + + // Single Row + if (!$blockKey) { + $tempArray[] = 0; + $blockKey = count($tempArray) - 1; + } + + // Remove the index from inside [data], to the front + array_splice($tempArray, 0, 0, $blockKey); + + // Check for nested data (elements, table) + if (preg_match('/data.(\d*\.\d*)/', $keyedIndex)) { + unset($tempArray[count($tempArray) - 2]); + } else { + array_pop($tempArray); + } + + FeedMeArrayHelper::arraySet($sortedData, $tempArray, $value); + } + } + + // Now a special case for field options. Because of the way field-mapping stored them, we need to + // loop through and apply across all blocks of this type. This also makes field-processing easier + foreach ($sortedData as $blockOrder => $blockData) { + foreach ($blockData as $blockHandle => $innerData) { + $optionData = Hash::get($optionsArray, $blockHandle); + + if ($optionData) { + $sortedData[$blockOrder][$blockHandle] = Hash::merge($innerData, $optionData); + } + } + } + + + // Sort by the new ordering we've set + ksort($sortedData); + + // Store the fields for this Matrix - can't use the fields service due to context + $blockTypes = craft()->matrix->getBlockTypesByFieldId($field->id, 'handle'); + + $count = 0; + $allPreppedFieldData = array(); + + foreach ($sortedData as $sortKey => $sortData) { + foreach ($sortData as $blockHandle => $blockFieldData) { + foreach ($blockFieldData as $blockFieldHandle => $blockFieldContent) { + + // Get the Matrix-contexted field for our regular field-prepping function + $blockType = $blockTypes[$blockHandle]; + + foreach ($blockType->getFields() as $f) { + if ($f->handle == $blockFieldHandle) { + $subField = $f; + } + } + + if (!isset($subField)) { + continue; + } + + $fieldOptions = array( + 'field' => $subField, + ); + + // Parse this inner-field's data, just like a regular field + $parsedData = craft()->feedMe_fields->prepForFieldType(null, $blockFieldContent, $blockFieldHandle, $fieldOptions); + + if ($parsedData) { + // Special-case for inner table - not a great solution at the moment, needs to be more flexible + if ($subField->type == 'Table') { + foreach ($parsedData as $i => $tableFieldRow) { + $next = reset($tableFieldRow); + + if (!is_array($next)) { + $tableFieldRow = array($i => $tableFieldRow); + } + + foreach ($tableFieldRow as $j => $tableFieldColumns) { + foreach ($tableFieldColumns as $k => $tableFieldColumn) { + $allPreppedFieldData[$k][$blockHandle][$blockFieldHandle][$j][$sortKey] = $tableFieldColumn; + } + } + } + } else { + $allPreppedFieldData[$sortKey][$blockHandle][$blockFieldHandle] = $parsedData; + } + } + } + } + } + + // Now we've got a bit more sane data - its a simple (regular) import + if ($allPreppedFieldData) { + foreach ($allPreppedFieldData as $key => $preppedBlockFieldData) { + foreach ($preppedBlockFieldData as $blockHandle => $preppedFieldData) { + $preppedData['new'.($count+1)] = array( + 'type' => $blockHandle, + 'order' => ($count+1), + 'enabled' => true, + 'fields' => $preppedFieldData, + ); + + $count++; + } + } + } + + return $preppedData; + } + + // Allows us to smartly-check to look at existing Matrix fields for an element, and whether data has changed or not. + // No need to update Matrix blocks unless content has changed, which causes needless new elements to be created. + public function postFieldData($element, $field, &$data, $handle) + { + $existingFieldData = array(); + $fieldData = $data[$handle]; + + // Get our Matrix blocks from the existing element + $blocks = $element->getFieldValue($field->handle); + + foreach ($blocks as $key => $block) { + $fieldValues = array(); + + // Get all the inner fields for this Matrix block + foreach ($block->getFieldLayout()->getFields() as $fieldLayoutField) { + $innerField = $fieldLayoutField->getField(); + + // Get the inner field content + $fieldValue = $block->getFieldValue($innerField->handle); + + // If we have an Element Criteria Model (Entries, Assets, etc), get the ids + if ($fieldValue instanceof ElementCriteriaModel) { + $fieldValue = $fieldValue->ids(); + } + + if ($fieldValue) { + $fieldValues[$innerField->handle] = $fieldValue; + } + } + + // Create an array of content so that it matches what we use to import - easy to compare this way + $existingFieldData['new'.($key+1)] = array( + 'type' => $block->type->handle, + 'order' => $block->sortOrder, + 'enabled' => $block->type->enabled, + 'fields' => $fieldValues, + ); + } + + + // Now, we should have identically formatted existing content to how we're about to import. + // Simply see if the arrays match exactly - size and attributes must be identical + if ($existingFieldData == $fieldData) { + // If they do equal, then nothing has changed from existing content. Se, we want to remove our mapped + // data from the feed entirely, so the element doesn't get updated (because it doesn't need to), + unset($data[$handle]); + } + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/MultiSelectFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/MultiSelectFeedMeFieldType.php new file mode 100644 index 00000000..5e7f1f47 --- /dev/null +++ b/feedme/FeedMe/FieldTypes/MultiSelectFeedMeFieldType.php @@ -0,0 +1,25 @@ +getFieldType()->getSettings(); + $options = $settings->getAttribute('options'); + + // find matching option label + foreach ($options as $option) { + if ($data == $option['value']) { + $preppedData = $option['value']; + break; + } + } + + return $preppedData; + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/TableFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/TableFeedMeFieldType.php new file mode 100644 index 00000000..8ae661a9 --- /dev/null +++ b/feedme/FeedMe/FieldTypes/TableFeedMeFieldType.php @@ -0,0 +1,108 @@ + $row) { + //$data[$columnHandle] = array($row); + } + } + + // And an even more special-case, when use it Matrix 'Matrix/MatrixItem/.../Table/Row/.../Column1' + // we need to process it a little differently. Notice the two repeatable nodes. + /*if (substr_count($options['feedHandle'][0], '/.../') == 2) { + $next = reset($data); + $next = reset($next); + + if (is_array($next)) { + foreach ($data as $i => $row) { + foreach ($row as $j => $column) { + foreach ($column as $k => $col) { + // Check for false for checkbox + if ($col === 'false') { + $col = null; + } + + $preppedData[$k][($j+1)][$i] = $col; + } + } + } + + return $preppedData; + } + }*/ + + foreach ($data as $i => $row) { + if (!isset($row['data'])) { + continue; + } + + if (!is_array($row['data'])) { + $row['data'] = array($row['data']); + } + + foreach ($row['data'] as $j => $column) { + // Check for false for checkbox + if ($column === 'false') { + $column = null; + } + + // Actually need to invert keys. Feed-mapping will deliver feed data as: + // array: { + // col1: { + // 0: val, + // 1: val, + // } + // col2: { + // 0: val, + // 1: val, + // } + // } + // We need to convert this to: + // array: { + // 0: { + // col1: val, + // col2: val, + // } + // 1: { + // col1: val, + // col2: val, + // } + // } + + $preppedData[($j+1)][$i] = $column; + } + } + + return $preppedData; + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/TagsFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/TagsFeedMeFieldType.php new file mode 100644 index 00000000..126ed96f --- /dev/null +++ b/feedme/FeedMe/FieldTypes/TagsFeedMeFieldType.php @@ -0,0 +1,113 @@ +getFieldType()->getSettings(); + + // Get tag group id + $source = $settings->getAttribute('source'); + list($type, $groupId) = explode(':', $source); + + // Find existing + foreach ($data as $tag) { + $criteria = craft()->elements->getCriteria(ElementType::Tag); + $criteria->status = null; + $criteria->groupId = $groupId; + $criteria->title = DbHelper::escapeParam($tag); + + $elements = $criteria->ids(); + + $preppedData = array_merge($preppedData, $elements); + + // Create the elements if we require + if (count($elements) == 0) { + if (isset($fieldData['options']['create'])) { + $preppedData[] = $this->_createElement($tag, $groupId); + } + } + } + + // Check if we've got any data for the fields in this element + if (isset($fieldData['fields'])) { + $this->_populateElementFields($preppedData, $fieldData['fields']); + } + + return $preppedData; + } + + + // Private Methods + // ========================================================================= + + private function _populateElementFields($tagData, $fieldData) + { + foreach ($tagData as $i => $tagId) { + $tag = craft()->tags->getTagById($tagId, null); + + // Prep each inner field + $preppedData = array(); + foreach ($fieldData as $fieldHandle => $fieldContent) { + $data = craft()->feedMe_fields->prepForFieldType(null, $fieldContent, $fieldHandle, null); + + if (is_array($data)) { + $data = Hash::get($data, $i); + } + + $preppedData[$fieldHandle] = $data; + } + + $tag->setContentFromPost($preppedData); + + if (!craft()->tags->saveTag($tag)) { + FeedMePlugin::log('Tag error: ' . json_encode($tag->getErrors()), LogLevel::Error, true); + } else { + FeedMePlugin::log('Updated Tag (ID ' . $tagId . ') inner-element with content: ' . json_encode($preppedData), LogLevel::Info, true); + } + } + } + + private function _createElement($tag, $groupId) + { + $element = new TagModel(); + $element->getContent()->title = $tag; + $element->groupId = $groupId; + + // Save tag + if (craft()->tags->saveTag($element)) { + return $element->id; + } else { + throw new Exception(json_encode($element->getErrors())); + } + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/FieldTypes/UsersFeedMeFieldType.php b/feedme/FeedMe/FieldTypes/UsersFeedMeFieldType.php new file mode 100644 index 00000000..f5d72a9e --- /dev/null +++ b/feedme/FeedMe/FieldTypes/UsersFeedMeFieldType.php @@ -0,0 +1,131 @@ +getFieldType()->getSettings(); + + // Get source id's for connecting + $groupIds = array(); + $sources = $settings->sources; + if (is_array($sources)) { + foreach ($sources as $source) { + list($type, $id) = explode(':', $source); + $groupIds[] = $id; + } + } + + // Find existing + foreach ($data as $user) { + $criteria = craft()->elements->getCriteria(ElementType::User); + $criteria->status = null; + $criteria->groupId = $groupIds; + $criteria->limit = $settings->limit; + + // Check if we've specified which attribute we're trying to match against + $attribute = Hash::get($fieldData, 'options.match', 'email'); + $criteria->$attribute = DbHelper::escapeParam($user); + $elements = $criteria->ids(); + + $preppedData = array_merge($preppedData, $elements); + + // Create the elements if we require + if (count($elements) == 0) { + if (isset($fieldData['options']['create'])) { + $preppedData[] = $this->_createElement($user, $groupIds); + } + } + } + + // Check for field limit - only return the specified amount + if ($preppedData) { + if ($field->settings['limit']) { + $preppedData = array_chunk($preppedData, $field->settings['limit']); + $preppedData = $preppedData[0]; + } + } + + // Check if we've got any data for the fields in this element + if (isset($fieldData['fields'])) { + $this->_populateElementFields($preppedData, $fieldData['fields']); + } + + return $preppedData; + } + + + + // Private Methods + // ========================================================================= + + private function _populateElementFields($userData, $fieldData) + { + foreach ($userData as $i => $userId) { + $user = craft()->users->getUserById($userId); + + // Prep each inner field + $preppedData = array(); + foreach ($fieldData as $fieldHandle => $fieldContent) { + $data = craft()->feedMe_fields->prepForFieldType(null, $fieldContent, $fieldHandle, null); + + if (is_array($data)) { + $data = Hash::get($data, $i); + } + + $preppedData[$fieldHandle] = $data; + } + + $user->setContentFromPost($preppedData); + + if (!craft()->users->saveUser($user)) { + FeedMePlugin::log('User error: ' . json_encode($user->getErrors()), LogLevel::Error, true); + } else { + FeedMePlugin::log('Updated User (ID ' . $userId . ') inner-element with content: ' . json_encode($preppedData), LogLevel::Info, true); + } + } + } + + private function _createElement($user, $groupIds) + { + $element = new UserModel(); + $element->email = $user; + $element->groupId = $groupIds; + + // Save category + if (craft()->users->saveUser($element)) { + return $element->id; + } else { + throw new Exception(json_encode($element->getErrors())); + } + } + +} \ No newline at end of file diff --git a/feedme/FeedMe/Helpers/FeedMeArrayHelper.php b/feedme/FeedMe/Helpers/FeedMeArrayHelper.php new file mode 100644 index 00000000..168beb65 --- /dev/null +++ b/feedme/FeedMe/Helpers/FeedMeArrayHelper.php @@ -0,0 +1,101 @@ + 1) { + $key = array_shift($keys); + + // If the key doesn't exist at this depth, we will just create an empty array + // to hold the next value, allowing us to create the arrays to hold final + // values at the correct depth. Then we'll keep digging into the array. + if (! isset($array[$key]) || ! is_array($array[$key])) { + $array[$key] = []; + } + + $array = &$array[$key]; + } + + $array[array_shift($keys)] = $value; + + return $array; + } + + public static function arrayGet($array, $key, $default = null) + { + if (is_null($key)) { + return $array; + } + + if (isset($array[$key])) { + return $array[$key]; + } + + // Store resulting array if key contains wildcard. + $deepArray = array(); + $keys = preg_split('/\^|\./', $key); + + foreach ($keys as $n => $segment) { + if ($segment == '*') { + // Get the rest of the keys besides current one. + $keySlice = array_slice($keys, $n+1); + // Generate new dot notation key string. + $innerKey = implode('^', $keySlice); + + if (is_array($array)) { + foreach ($array as $d => $item) { + // Empty slice - last segment is a wildcard. + if (empty($keySlice)) { + // Last segment is a wildcard. Put item into deepArray which will be returned + // containing all of the items of the current array. + $deepArray[] = $item; + } else { + // Pass current array item deeper. + $innerItem = FeedMeArrayHelper::arrayGet($item, $innerKey, $default); + + if (is_array($innerItem) and count(array_keys($keys, '*')) > 1) { + // Multiple wildcards, add each item of inner array to the resulting new array. + foreach ($innerItem as $innerItem) { + $deepArray[$d][] = $innerItem; + } + } else { + // Only one wildcard in current key string. Add whole inner array to the resulting array. + $deepArray[] = $innerItem; + } + } + } + + // Return new resulting array. + return $deepArray; + } elseif ($n == count($keys) - 1) { + // This is the last key, so we can simply return whole array. + return $array; + } else { + // This is not the last key and $array is not really an array + // so we can't proceed deeper. Return default. + return $default; + } + } elseif (!is_array($array) || !array_key_exists($segment, $array)) { + return $default; + } + + $array = $array[$segment]; + } + + return $array; + } +} \ No newline at end of file diff --git a/feedme/FeedMe/Helpers/FeedMeDuplicate.php b/feedme/FeedMe/Helpers/FeedMeDuplicate.php new file mode 100644 index 00000000..4ebeea5c --- /dev/null +++ b/feedme/FeedMe/Helpers/FeedMeDuplicate.php @@ -0,0 +1,54 @@ +requestProduct = $product; + $this->requestVersion = $productVersion; + + $endpoint .= craft()->config->get('endpointSuffix'); + $this->_endpoint = $endpoint; + $userEmail = craft()->userSession->getUser() ? craft()->userSession->getUser()->email : ''; + + // Cater for pre-Craft 2.6.2951 + if (version_compare(craft()->getVersion(), '2.6.2951', '<')) { + $version = craft()->getVersion() . '.' . craft()->getBuild(); + } else { + $version = craft()->getVersion(); + } + + $attributes = array( + 'requestUrl' => craft()->request->getHostInfo() . craft()->request->getUrl(), + 'requestIp' => craft()->request->getIpAddress(), + 'requestTime' => DateTimeHelper::currentTimeStamp(), + 'requestPort' => craft()->request->getPort(), + + 'craftVersion' => $version, + 'craftEdition' => craft()->getEdition(), + 'userEmail' => $userEmail, + + 'requestProduct' => $this->requestProduct, + 'requestVersion' => $this->requestVersion, + 'licenseKey' => $licenseKey + ); + + $this->_model = new FeedMe_LicenseModel($attributes); + + $this->_userAgent = 'Craft/' . $version; + } + + public function getTimeout() + { + return $this->_timeout; + } + + public function getConnectTimeout() + { + return $this->_connectTimeout; + } + + public function setAllowRedirects($allowRedirects) + { + $this->_allowRedirects = $allowRedirects; + } + + public function getAllowRedirects() + { + return $this->_allowRedirects; + } + + public function getModel() + { + return $this->_model; + } + + public function setData($data) + { + $this->_model->data = $data; + } + + public function setHandle($handle) + { + $this->_model->handle = $handle; + } + + public function phoneHome($force = false) + { + if ($force) { + if (craft()->cache->get($this->etConnectFailureKey)) { + craft()->cache->delete($this->etConnectFailureKey); + } + if (craft()->cache->get($this->etRecentPhoneHome)) { + craft()->cache->delete($this->etRecentPhoneHome); + } + } + + try { + + if (!craft()->cache->get($this->etConnectFailureKey)) { + $data = $this->_model->getAttributes(null, true); + + $client = new \Guzzle\Http\Client(); + $client->setUserAgent($this->_userAgent, true); + + $options = array( + 'timeout' => $this->getTimeout(), + 'connect_timeout' => $this->getConnectTimeout(), + 'allow_redirects' => $this->getAllowRedirects(), + ); + + $request = $client->post($this->_endpoint, null, $data, $options); + + // Potentially long-running request, so close session to prevent session blocking on subsequent requests. + craft()->session->close(); + + $response = $request->send(); + + if ($response->isSuccessful()) { + + // Clear the connection failure cached item if it exists. + if (craft()->cache->get($this->etConnectFailureKey)) { + craft()->cache->delete($this->etConnectFailureKey); + } + + // Clear the connection failure cached item if it exists. + craft()->cache->set($this->etRecentPhoneHome, true, 300); + + $etModel = craft()->feedMe_license->decodeEtModel($response->getBody()); + + if ($etModel) { + return $etModel; + } else { + FeedMePlugin::log('Error in calling ' . $this->_endpoint . ' Response: ' . $response->getBody(), LogLevel::Warning); + + if (craft()->cache->get($this->etConnectFailureKey)) { + // There was an error, but at least we connected. + craft()->cache->delete($this->etConnectFailureKey); + } + } + } else { + FeedMePlugin::log('Error in calling ' . $this->_endpoint . ' Response: ' . $response->getBody(), LogLevel::Warning); + + if (craft()->cache->get($this->etConnectFailureKey)) { + // There was an error, but at least we connected. + craft()->cache->delete($this->etConnectFailureKey); + } + } + } + } // Let's log and rethrow any EtExceptions. + catch (EtException $e) { + FeedMePlugin::log('Error in ' . __METHOD__ . '. Message: ' . $e->getMessage(), LogLevel::Error); + + if (craft()->cache->get($this->etConnectFailureKey)) { + // There was an error, but at least we connected. + craft()->cache->delete($this->etConnectFailureKey); + } + + throw $e; + } catch (\Exception $e) { + FeedMePlugin::log('Error in ' . __METHOD__ . '. Message: ' . $e->getMessage(), LogLevel::Error); + + // Cache the failure for 5 minutes so we don't try again. + craft()->cache->set($this->etConnectFailureKey, true, 300); + } + + return null; + } + +} diff --git a/feedme/FeedMePlugin.php b/feedme/FeedMePlugin.php index eec0c4f8..b47dad2a 100644 --- a/feedme/FeedMePlugin.php +++ b/feedme/FeedMePlugin.php @@ -1,12 +1,7 @@ templates->render('feedme/settings', array( - 'settings' => $this->getSettings() - )); + return 'feedme/settings'; } - protected function defineSettings() + public function registerCpRoutes() { return array( - 'pluginNameOverride' => AttributeType::String, - 'cache' => array(AttributeType::Number, 'default' => 60), - 'enabledTabs' => array(AttributeType::Mixed, 'default' => true), + 'feedme' => array('action' => 'feedMe/feeds/feedsIndex'), + 'feedme/feeds' => array('action' => 'feedMe/feeds/feedsIndex'), + 'feedme/feeds/new' => array('action' => 'feedMe/feeds/editFeed'), + 'feedme/feeds/(?P\d+)' => array('action' => 'feedMe/feeds/editFeed'), + 'feedme/feeds/map/(?P\d+)' => array('action' => 'feedMe/feeds/mapFeed'), + 'feedme/feeds/run/(?P\d+)' => array('action' => 'feedMe/feeds/runFeed'), + 'feedme/logs' => array('action' => 'feedMe/logs/logs'), + 'feedme/settings/general' => array('action' => 'feedMe/settings'), + 'feedme/settings/license' => array('action' => 'feedMe/license/edit'), ); } - public function registerCpRoutes() + protected function defineSettings() { return array( - 'feedme' => array('action' => 'FeedMe/feeds/feedsIndex'), - 'feedme/feeds' => array('action' => 'FeedMe/feeds/feedsIndex'), - 'feedme/feeds/new' => array('action' => 'FeedMe/feeds/editFeed'), - 'feedme/feeds/(?P\d+)' => array('action' => 'FeedMe/feeds/editFeed'), - 'feedme/runTask/(?P\d+)' => array('action' => 'FeedMe/feeds/runTask'), - - 'feedme/logs' => array('action' => 'FeedMe/logs/logs'), + 'pluginNameOverride' => AttributeType::String, + 'cache' => array(AttributeType::Number, 'default' => 60), + 'enabledTabs' => array(AttributeType::Mixed, 'default' => true), + 'edition' => array(AttributeType::Mixed), ); } public function onBeforeInstall() - { + { + $version = craft()->getVersion(); + + // Craft 2.6.2951 deprecated `craft()->getBuild()`, so get the version number consistently + if (version_compare(craft()->getVersion(), '2.6.2951', '<')) { + $version = craft()->getVersion() . '.' . craft()->getBuild(); + } + // Craft 2.3.2636 fixed an issue with BaseEnum::getConstants() - if (version_compare(craft()->getVersion() . '.' . craft()->getBuild(), '2.3.2636', '<')) { + if (version_compare($version, '2.3.2636', '<')) { throw new Exception($this->getName() . ' requires Craft CMS 2.3.2636+ in order to run.'); } } + public function onAfterInstall() + { + craft()->request->redirect(UrlHelper::getCpUrl('feedme/welcome')); + } + + public function init() + { + Craft::import('plugins.feedme.FeedMe.DataTypes.*'); + Craft::import('plugins.feedme.FeedMe.ElementTypes.*'); + Craft::import('plugins.feedme.FeedMe.FieldTypes.*'); + Craft::import('plugins.feedme.FeedMe.License.*'); + Craft::import('plugins.feedme.FeedMe.Helpers.*'); + + if (craft()->request->isCpRequest()) { + craft()->feedMe_license->ping(); + } + } + // ========================================================================= // HOOKS // ========================================================================= - public function addTwigExtension() + // Native Data Type Support + public function registerFeedMeDataTypes() + { + return array( + new JsonFeedMeDataType(), + new XmlFeedMeDataType(), + ); + } + + // Native Element Type Support + public function registerFeedMeElementTypes() { - Craft::import('plugins.feedme.twigextensions.UniqidTwigExtension'); - return new UniqidTwigExtension(); + if (craft()->feedMe_license->isProEdition()) { + return array( + new AssetFeedMeElementType(), + new CategoryFeedMeElementType(), + new Commerce_ProductFeedMeElementType(), + new EntryFeedMeElementType(), + new UserFeedMeElementType(), + ); + } else { + return array( + new EntryFeedMeElementType(), + ); + } + } + + // Native Field Type Support + public function registerFeedMeFieldTypes() + { + return array( + new AssetsFeedMeFieldType(), + new CategoriesFeedMeFieldType(), + new CheckboxesFeedMeFieldType(), + new DateFeedMeFieldType(), + new DefaultFeedMeFieldType(), + new DropdownFeedMeFieldType(), + new EntriesFeedMeFieldType(), + new LightswitchFeedMeFieldType(), + new MatrixFeedMeFieldType(), + new MultiSelectFeedMeFieldType(), + new NumberFeedMeFieldType(), + new RadioButtonsFeedMeFieldType(), + new TableFeedMeFieldType(), + new TagsFeedMeFieldType(), + new UsersFeedMeFieldType(), + ); } } diff --git a/feedme/composer.json b/feedme/composer.json new file mode 100644 index 00000000..ddee175c --- /dev/null +++ b/feedme/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "cakephp/utility": "3.3.12" + } +} diff --git a/feedme/composer.lock b/feedme/composer.lock new file mode 100644 index 00000000..c210532b --- /dev/null +++ b/feedme/composer.lock @@ -0,0 +1,57 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "2a3d130b8e090cfcc809df51084181ba", + "packages": [ + { + "name": "cakephp/utility", + "version": "3.3.12", + "source": { + "type": "git", + "url": "https://github.com/cakephp/utility.git", + "reference": "88043751b4da6745761ce9cab1cf95256138d8b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/utility/zipball/88043751b4da6745761ce9cab1cf95256138d8b9", + "reference": "88043751b4da6745761ce9cab1cf95256138d8b9", + "shasum": "" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Utility\\": "." + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "http://cakephp.org" + } + ], + "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security", + "time": "2017-01-06T13:46:45+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/feedme/controllers/FeedMeController.php b/feedme/controllers/FeedMeController.php index b6edb5ce..2d071c80 100644 --- a/feedme/controllers/FeedMeController.php +++ b/feedme/controllers/FeedMeController.php @@ -3,23 +3,27 @@ class FeedMeController extends BaseController { + protected $allowAnonymous = array('actionClearTasks'); + // Public Methods // ========================================================================= - public function actionGetEntryTypes() + public function actionSettings() { - // Only ajax post requests - $this->requirePostRequest(); - $this->requireAjaxRequest(); + $settings = craft()->feedMe->getSettings(); - // Get section - $section = craft()->request->getPost('section'); - $section = craft()->sections->getSectionById($section); + $this->renderTemplate('feedme/settings/general', array( + 'settings' => $settings, + )); + } - // Get entry types - $entrytypes = $section->getEntryTypes(); + public function actionClearTasks() + { + // Function to clear (delete) all stuck tasks. + craft()->db->createCommand()->delete('tasks'); - // Return JSON - $this->returnJson($entrytypes); + $this->redirect(craft()->request->getUrlReferrer()); } + + } diff --git a/feedme/controllers/FeedMe_FeedsController.php b/feedme/controllers/FeedMe_FeedsController.php index f03bea69..4b97e5a2 100644 --- a/feedme/controllers/FeedMe_FeedsController.php +++ b/feedme/controllers/FeedMe_FeedsController.php @@ -21,63 +21,78 @@ public function actionFeedsIndex() public function actionEditFeed(array $variables = array()) { - if (!empty($variables['feedId'])) { - $variables['feed'] = craft()->feedMe_feeds->getFeedById($variables['feedId']); - } else { - $variables['feed'] = new FeedMe_FeedModel(); + if (empty($variables['feed'])) { + if (!empty($variables['feedId'])) { + $variables['feed'] = craft()->feedMe_feeds->getFeedById($variables['feedId']); + } else { + $variables['feed'] = new FeedMe_FeedModel(); + $variables['feed']->passkey = StringHelper::randomString(10); + } } $this->renderTemplate('feedme/feeds/_edit', $variables); } - public function getModelFromPost() { - $this->requirePostRequest(); + public function actionMapFeed(array $variables = array()) + { + if (empty($variables['feed'])) { + $feed = craft()->feedMe_feeds->getFeedById($variables['feedId']); + $feedData = craft()->feedMe_data->getFeedMapping($feed->feedType, $feed->feedUrl, $feed->primaryElement, $feed); - $feed = new FeedMe_FeedModel(); + $variables['feed'] = $feed; - // Shared attributes - if (craft()->request->getPost('feedId')) { - $feed->id = craft()->request->getPost('feedId'); + if ($feedData) { + $variables['feedRawData'] = $feedData; + } } - $feed->name = craft()->request->getPost('name'); - $feed->feedUrl = craft()->request->getPost('feedUrl'); - $feed->feedType = craft()->request->getPost('feedType'); - $feed->primaryElement = craft()->request->getPost('primaryElement'); - $feed->section = craft()->request->getPost('section'); - $feed->entrytype = craft()->request->getPost('entrytype'); - $feed->locale = craft()->request->getPost('locale'); - $feed->duplicateHandle = craft()->request->getPost('duplicateHandle'); - $feed->passkey = craft()->request->getPost('passkey'); - $feed->backup = craft()->request->getPost('backup'); + $this->renderTemplate('feedme/feeds/_map', $variables); + } - // Don't overwrite mappings when saving from first screen - if (craft()->request->getPost('fieldMapping')) { - $feed->fieldMapping = craft()->request->getPost('fieldMapping'); - } - if (craft()->request->getPost('fieldUnique')) { - $feed->fieldUnique = craft()->request->getPost('fieldUnique'); - } + public function actionRunFeed(array $variables = array()) + { + $feed = craft()->feedMe_feeds->getFeedById($variables['feedId']); - return $feed; + $variables['feed'] = $feed; + $variables['task'] = $this->_runImportTask($feed->id); + + if (craft()->request->getParam('direct')) { + // If the user triggers this from the control panel (maybe for testing), triggering a task immediately will + // lock up the browser session while it runs. In that case, we use JS to trigger the task (in _direct template) + // + // However, when triggering via Cron, run the task immediately, as Cron doesn't trigger JS (there's no browser) + // Best way to check if its being run from a non-browser, as each server is different, so can't be sure what they trigger with + $browser = $this->_getBrowserName(craft()->request->getUserAgent()); + + if ($browser == 'Other') { + craft()->tasks->runPendingTasks(); + } + + $this->renderTemplate('feedme/feeds/_direct', $variables); + } else { + $this->renderTemplate('feedme/feeds/_run', $variables); + } } public function actionSaveFeed() { - $feed = $this->getModelFromPost(); + $feed = $this->_getModelFromPost(); - // Save it - if (craft()->feedMe_feeds->saveFeed($feed)) { - craft()->userSession->setNotice(Craft::t('Feed saved.')); - $this->redirect('feedme/feeds'); - } else { - craft()->userSession->setError(Craft::t('Couldn’t save feed.')); - } + $this->_saveAndRedirect($feed, 'feedme/feeds/', true); + } + + public function actionSaveAndMapFeed() + { + $feed = $this->_getModelFromPost(); + + $this->_saveAndRedirect($feed, 'feedme/feeds/map/', true); + } + + public function actionSaveAndImportFeed() + { + $feed = $this->_getModelFromPost(); - // Send the feed back to the template - craft()->urlManager->setRouteVariables(array( - 'feed' => $feed - )); + $this->_saveAndRedirect($feed, 'feedme/feeds/run/', true); } public function actionDeleteFeed() @@ -91,59 +106,32 @@ public function actionDeleteFeed() $this->returnJson(array('success' => true)); } - public function actionMapFeed() + public function actionRunTask(array $variables = array()) { - $feed = $this->getModelFromPost(); - - // We're onto the mapping step, but lets save what we've got so far anyway. - if (craft()->feedMe_feeds->saveFeed($feed)) { - craft()->userSession->setNotice(Craft::t('Feed saved.')); - - // Get the data for the mapping screen, based on the URL provided - $feedData = craft()->feedMe_feed->getFeedMapping($feed->feedType, $feed->feedUrl, $feed->primaryElement); - - if ($feedData) { - $this->renderTemplate('feedme/feeds/_map', array( - 'feed' => $feed, - 'feedData' => $feedData, - )); - } - } else { - craft()->userSession->setError(Craft::t('Couldn’t save feed.')); + if (craft()->request->getParam('feedId')) { + $variables = array('feedId' => craft()->request->getParam('feedId')); + $this->actionRunFeed($variables); } } - public function actionPerformFeed() + public function actionDebug() { - $feed = $this->getModelFromPost(); + $feedId = craft()->request->getParam('feedId'); + $limit = craft()->request->getParam('limit'); - // Mapping and all other setting are ready to go. Save and proceed with actual feed - if (craft()->feedMe_feeds->saveFeed($feed)) { - craft()->userSession->setNotice(Craft::t('Feed saved.')); - - // Feed settings have saved, now we're ready to trigger the import - $this->runImportTask($feed->id); - } else { - craft()->userSession->setError(Craft::t('Couldn’t save feed.')); - } + craft()->feedMe_process->debugFeed($feedId, $limit); + craft()->end(); } - public function actionRunTask(array $variables = array()) - { - if (!empty($variables['feedId'])) { - $feedId = $variables['feedId']; - } else if (craft()->request->getParam('feedId')) { - $feedId = craft()->request->getParam('feedId'); - } else { - $feedId = null; - } + + - if ($feedId) { - $this->runImportTask($feedId); - } - } - public function runImportTask($feedId) { + // Private Methods + // ========================================================================= + + private function _runImportTask($feedId) + { $feed = craft()->feedMe_feeds->getFeedById($feedId); $settings = array( @@ -160,48 +148,118 @@ public function runImportTask($feedId) { // Create the import task craft()->tasks->createTask('FeedMe', $feed->name, $settings); - // Trigger the task to run right now! - $this->_runPendingTasks(); - - // if not using the direct param for this request, so UI stuff + // if not using the direct param for this request, do UI stuff craft()->userSession->setNotice(Craft::t('Feed processing started.')); - - $this->redirect('feedme/feeds'); } // If not, are we running directly? if (craft()->request->getParam('direct')) { - if (craft()->request->getParam('passkey') == $feed['passkey']) { - // Create the import task + $proceed = craft()->request->getParam('passkey') == $feed['passkey']; + + // Create the import task only if provided the correct passkey + if ($proceed) { craft()->tasks->createTask('FeedMe', $feed->name, $settings); + } - // Trigger the task to run right now! - $this->_runPendingTasks(); + return $proceed; + } + } - // Let the requester know whats going on. - $this->returnJson(array('success' => 'Feed ID: '.$feed['id'].' - Task started')); - } else { - $this->returnJson(array('error' => 'Invalid Passkey')); + private function _saveAndRedirect($feed, $redirect, $withId = false) + { + if (craft()->feedMe_feeds->saveFeed($feed)) { + craft()->userSession->setNotice(Craft::t('Feed saved.')); + + if ($withId) { + $redirect = $redirect . $feed->id; } + + $this->redirect($redirect); + } else { + craft()->userSession->setError(Craft::t('Couldn’t save feed: ' . implode($feed->getAllErrors(), ' '))); } - + craft()->urlManager->setRouteVariables(array('feed' => $feed)); } + private function _getModelFromPost() + { + $this->requirePostRequest(); + if (craft()->request->getPost('feedId')) { + $feed = craft()->feedMe_feeds->getFeedById(craft()->request->getPost('feedId')); + } else { + $feed = new FeedMe_FeedModel(); + } - // Private Methods - // ========================================================================= + $feed->name = craft()->request->getRequiredPost('name', $feed->name); + $feed->feedUrl = craft()->request->getRequiredPost('feedUrl', $feed->feedUrl); + $feed->feedType = craft()->request->getRequiredPost('feedType', $feed->feedType); + $feed->primaryElement = craft()->request->getPost('primaryElement', $feed->primaryElement); + $feed->elementType = craft()->request->getRequiredPost('elementType', $feed->elementType); + $feed->elementGroup = craft()->request->getPost('elementGroup', $feed->elementGroup); + $feed->locale = craft()->request->getPost('locale', $feed->locale); + $feed->duplicateHandle = craft()->request->getPost('duplicateHandle', $feed->duplicateHandle); + $feed->passkey = craft()->request->getRequiredPost('passkey', $feed->passkey); + $feed->backup = craft()->request->getPost('backup', $feed->backup); - private function _runPendingTasks() - { - if (!craft()->tasks->isTaskRunning()) { - $task = craft()->tasks->getNextPendingTask(); + // Don't overwrite mappings when saving from first screen + if (craft()->request->getPost('fieldMapping')) { + $feed->fieldMapping = craft()->request->getPost('fieldMapping'); + } + + if (craft()->request->getPost('fieldDefaults')) { + $feed->fieldDefaults = craft()->request->getPost('fieldDefaults'); + } + + if (craft()->request->getPost('fieldElementMapping')) { + $feed->fieldElementMapping = craft()->request->getPost('fieldElementMapping'); + } + + if (craft()->request->getPost('fieldElementDefaults')) { + $feed->fieldElementDefaults = craft()->request->getPost('fieldElementDefaults'); + } + + if (craft()->request->getPost('fieldUnique')) { + $feed->fieldUnique = craft()->request->getPost('fieldUnique'); + } - if ($task) { - craft()->tasks->runPendingTasks(); + // Check conditionally on Element Group fields - depending on the Element Type selected + if (isset($feed->elementGroup[$feed->elementType])) { + $elementGroup = $feed->elementGroup[$feed->elementType]; + + if ($feed->elementType == 'Category') { + if (empty($elementGroup)) { + $feed->addError('elementGroup', Craft::t('Category Group is required')); + } + } + + if ($feed->elementType == 'Entry') { + if (empty($elementGroup['section']) || empty($elementGroup['entryType'])) { + $feed->addError('elementGroup', Craft::t('Entry Section and Type are required')); + } + } + + if ($feed->elementType == 'Commerce_Product') { + if (empty($elementGroup)) { + $feed->addError('elementGroup', Craft::t('Commerce Product Type is required')); + } } } + + return $feed; + } + + private function _getBrowserName($user_agent) + { + if (strpos($user_agent, 'Opera') || strpos($user_agent, 'OPR/')) return 'Opera'; + elseif (strpos($user_agent, 'Edge')) return 'Edge'; + elseif (strpos($user_agent, 'Chrome')) return 'Chrome'; + elseif (strpos($user_agent, 'Safari')) return 'Safari'; + elseif (strpos($user_agent, 'Firefox')) return 'Firefox'; + elseif (strpos($user_agent, 'MSIE') || strpos($user_agent, 'Trident/7')) return 'Internet Explorer'; + + return 'Other'; } } diff --git a/feedme/controllers/FeedMe_SupportController.php b/feedme/controllers/FeedMe_HelpController.php similarity index 84% rename from feedme/controllers/FeedMe_SupportController.php rename to feedme/controllers/FeedMe_HelpController.php index 8125ec12..0fb52ca0 100644 --- a/feedme/controllers/FeedMe_SupportController.php +++ b/feedme/controllers/FeedMe_HelpController.php @@ -1,7 +1,7 @@ plugins->getPlugin('feedMe'); $feed = craft()->feedMe_feeds->getFeedById($getHelpModel->feedIssue); + // Cater for pre-Craft 2.6.2951 + if (version_compare(craft()->getVersion(), '2.6.2951', '<')) { + $version = craft()->getVersion() . '.' . craft()->getBuild(); + } else { + $version = craft()->getVersion(); + } + // Add some extra info about this install $message = $getHelpModel->message . "\n\n" . "------------------------------\n\n" . - 'Craft '.craft()->getEditionName().' '.craft()->getVersion().'.'.craft()->getBuild() . "\n\n" . - 'Feed Me '.$plugin->getVersion(); + 'Craft '.craft()->getEditionName().' '.$version . "\n\n" . + 'Feed Me '.$plugin->getVersion() . "\n\n" . + 'License Key: '.craft()->feedMe_license->getLicenseKey(); try { $zipFile = $this->_createZip(); @@ -83,7 +91,10 @@ public function actionSendSupportRequest() // Save the contents of the feed // if ($getHelpModel->attachFeed) { - $feedData = craft()->feedMe_feed->getRawData($feed->feedUrl); + // Check for and environment variables in url + $url = craft()->config->parseEnvironmentString($feed->feedUrl); + + $feedData = craft()->feedMe_data->getRawData($url); $tempFile = $tempFolder.'feed.'.StringHelper::toLowerCase($feed->feedType); @@ -100,11 +111,17 @@ public function actionSendSupportRequest() if ($getHelpModel->attachFields) { $fieldInfo = array(); - foreach ($feed->fieldMapping as $feedHandle => $fieldHandle) { - $field = craft()->fields->getFieldByHandle($fieldHandle); + foreach ($feed->fieldMapping as $fieldHandle => $feedHandle) { + if ($fieldHandle && !is_array($fieldHandle)) { + // Check for sub-fields and options + $fieldHandleInfo = explode('-', $fieldHandle); + $fieldHandle = $fieldHandleInfo[0]; - if ($field) { - $fieldInfo[] = $this->_prepareExportField($field); + $field = craft()->fields->getFieldByHandle($fieldHandle); + + if ($field && !isset($fieldInfo[$field->handle])) { + $fieldInfo[$field->handle] = $this->_prepareExportField($field); + } } } @@ -139,7 +156,7 @@ public function actionSendSupportRequest() $email = new EmailModel(); $email->fromEmail = $getHelpModel->fromEmail; - $email->toEmail = "web@sgroup.com.au"; + $email->toEmail = "support@sgroup.com.au"; $email->subject = "Feed Me Support"; $email->body = $message; @@ -169,9 +186,10 @@ public function actionSendSupportRequest() $errors = $getHelpModel->getErrors(); } - $this->returnJson(array( + $this->renderTemplate('feedMe/help/response', array( 'success' => $success, - 'errors' => $errors, + 'errors' => JsonHelper::encode($errors), + 'widgetId' => 'feedMeHelp', )); } @@ -219,9 +237,7 @@ private function _prepareSqlFeedSettings($id) private function _prepareExportField($field) { - $fieldDefs = array(); - - $fieldDefs[$field->handle] = array( + $fieldDefs = array( 'name' => $field->name, 'context' => $field->context, 'instructions' => $field->instructions, @@ -253,7 +269,7 @@ private function _prepareExportField($field) ); } - $fieldDefs[$field->handle]['blockTypes'] = $blockTypeDefs; + $fieldDefs['blockTypes'] = $blockTypeDefs; } if ($field->type == 'SuperTable') { @@ -278,7 +294,7 @@ private function _prepareExportField($field) ); } - $fieldDefs[$field->handle]['blockTypes'] = $blockTypeDefs; + $fieldDefs['blockTypes'] = $blockTypeDefs; } return $fieldDefs; diff --git a/feedme/controllers/FeedMe_LicenseController.php b/feedme/controllers/FeedMe_LicenseController.php new file mode 100644 index 00000000..9dbc7147 --- /dev/null +++ b/feedme/controllers/FeedMe_LicenseController.php @@ -0,0 +1,87 @@ +feedMe_license->getLicenseKey(); + + $this->renderTemplate('feedme/settings/license', [ + 'hasLicenseKey' => ($licenseKey !== null) + ]); + } + + public function actionGetLicenseInfo() + { + $this->requirePostRequest(); + $this->requireAjaxRequest(); + + return $this->_sendResponse(craft()->feedMe_license->getLicenseInfo()); + } + + public function actionUnregister() + { + $this->requirePostRequest(); + $this->requireAjaxRequest(); + + return $this->_sendResponse(craft()->feedMe_license->unregisterLicenseKey()); + } + + public function actionTransfer() + { + $this->requirePostRequest(); + $this->requireAjaxRequest(); + + return $this->_sendResponse(craft()->feedMe_license->transferLicenseKey()); + } + + public function actionUpdateLicenseKey() + { + $this->requirePostRequest(); + $this->requireAjaxRequest(); + + $licenseKey = craft()->request->getRequiredPost('licenseKey'); + + // Are we registering a new license key? + if ($licenseKey) { + // Record the license key locally + try { + craft()->feedMe_license->setLicenseKey($licenseKey); + } catch (InvalidLicenseKeyException $e) { + $this->returnErrorJson(Craft::t('The license key is invalid.')); + } + + return $this->_sendResponse(craft()->feedMe_license->registerPlugin($licenseKey)); + } else { + // Just clear our record of the license key + craft()->feedMe_license->setLicenseKey(null); + craft()->feedMe_license->setLicenseKeyStatus(LicenseKeyStatus::Unknown); + return $this->_sendResponse(); + + } + } + + + // Private Methods + // ========================================================================= + + private function _sendResponse($success = true) + { + if ($success) { + $this->returnJson(array( + 'success' => true, + 'licenseKey' => craft()->feedMe_license->getLicenseKey(), + 'licenseKeyStatus' => craft()->plugins->getPluginLicenseKeyStatus('FeedMe'), + )); + } else { + //$this->returnErrorJson(craft()->feedMe_license->error); + $this->returnErrorJson(Craft::t('An unknown error occurred.')); + } + } + +} diff --git a/feedme/controllers/FeedMe_LogsController.php b/feedme/controllers/FeedMe_LogsController.php index a0aed2df..31154a04 100644 --- a/feedme/controllers/FeedMe_LogsController.php +++ b/feedme/controllers/FeedMe_LogsController.php @@ -3,6 +3,12 @@ class FeedMe_LogsController extends BaseController { + // Properties + // ========================================================================= + + private $_currentLogFileName = 'feedme.log'; + + // Public Methods // ========================================================================= @@ -14,14 +20,13 @@ public function actionLogs() $dateTimePattern = '/^[0-9]{4}\/[0-9]{2}\/[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/'; $logEntries = array(); - $currentLogFileName = 'feedme.log'; - $currentFullPath = craft()->path->getLogPath().$currentLogFileName; + $currentFullPath = craft()->path->getLogPath().$this->_currentLogFileName; if (IOHelper::fileExists($currentFullPath)) { // Split the log file's contents up into arrays of individual logs, where each item is an array of // the lines of that log. - $contents = IOHelper::getFileContents(craft()->path->getLogPath().$currentLogFileName); + $contents = IOHelper::getFileContents(craft()->path->getLogPath().$this->_currentLogFileName); $requests = explode('******************************************************************************************************', $contents); @@ -61,4 +66,12 @@ public function actionLogs() )); } } + + public function actionClear() + { + $currentFullPath = craft()->path->getLogPath() . $this->_currentLogFileName; + IOHelper::deleteFile($currentFullPath, true); + + craft()->request->redirect(craft()->request->urlReferrer); + } } diff --git a/feedme/enums/FeedMe_Duplicate.php b/feedme/enums/FeedMe_Duplicate.php deleted file mode 100644 index 148c645b..00000000 --- a/feedme/enums/FeedMe_Duplicate.php +++ /dev/null @@ -1,9 +0,0 @@ -dbConnection->schema->getTable('{{feedme_feeds}}'); + + if (!craft()->db->columnExists('feedme_feeds', 'elementType')) { + craft()->db->createCommand()->addColumnAfter('feedme_feeds', 'elementType', ColumnType::Varchar, 'primaryElement'); + } + + if (!craft()->db->columnExists('feedme_feeds', 'elementGroup')) { + craft()->db->createCommand()->addColumnAfter('feedme_feeds', 'elementGroup', ColumnType::Text, 'primaryElement'); + } + + if (!craft()->db->columnExists('feedme_feeds', 'fieldDefaults')) { + craft()->db->createCommand()->addColumnAfter('feedme_feeds', 'fieldDefaults', ColumnType::Text, 'fieldMapping'); + } + + if (!craft()->db->columnExists('feedme_feeds', 'fieldElementMapping')) { + craft()->db->createCommand()->addColumnAfter('feedme_feeds', 'fieldElementMapping', ColumnType::Text, 'fieldMapping'); + } + + if (!craft()->db->columnExists('feedme_feeds', 'fieldElementDefaults')) { + craft()->db->createCommand()->addColumnAfter('feedme_feeds', 'fieldElementDefaults', ColumnType::Text, 'fieldMapping'); + } + + + // Remove logging tables + if (craft()->db->tableExists('feedme_logs')) { + craft()->db->createCommand('SET FOREIGN_KEY_CHECKS = 0;')->execute(); + craft()->db->createCommand()->dropTable('feedme_logs'); + craft()->db->createCommand('SET FOREIGN_KEY_CHECKS = 1;')->execute(); + } + + if (craft()->db->tableExists('feedme_log')) { + craft()->db->createCommand('SET FOREIGN_KEY_CHECKS = 0;')->execute(); + craft()->db->createCommand()->dropTable('feedme_log'); + craft()->db->createCommand('SET FOREIGN_KEY_CHECKS = 1;')->execute(); + } + + // Grab current feedme data + $currentData = craft()->db->createCommand() + ->select('*') + ->from('feedme_feeds') + ->queryAll(); + + // Move Section/EntryType IDs into ElementGroup column + craft()->db->createCommand()->update('feedme_feeds', array('elementType' => 'Entry')); + + foreach ($currentData as $data) { + if (isset($data['section']) && isset($data['entrytype'])) { + $elementGroup = array( + 'Category' => '', + 'Entry' => array( + 'section' => $data['section'], + 'entryType' => $data['entrytype'], + ), + 'Commerce_Product' => '', + 'User' => '', + ); + + craft()->db->createCommand()->update('feedme_feeds', array('elementGroup' => json_encode($elementGroup)), 'id = ' . $data['id']); + } + } + + // Delete old columns + if ($table->getColumn('section') !== null) { + $this->dropColumn('feedme_feeds', 'section'); + } + if ($table->getColumn('entrytype') !== null) { + $this->dropColumn('feedme_feeds', 'entrytype'); + } + + // Loop through existing mapping - reverse values and keys + foreach ($currentData as $data) { + $newMapping = array(); + + $fieldMapping = json_decode($data['fieldMapping'], true); + + if ($fieldMapping) { + foreach ($fieldMapping as $feedHandle => $handle) { + $newFieldHandle = $handle; + + if (preg_match('/^(.*)\[(.*)]$/', $newFieldHandle, $matches)) { + $fieldHandle = $matches[1]; + $subfieldHandle = $matches[2]; + + $newFieldHandle = $fieldHandle . '--' . $subfieldHandle; + } + + $newMapping[$newFieldHandle] = $feedHandle; + } + } + + craft()->db->createCommand()->update('feedme_feeds', array('fieldMapping' => json_encode($newMapping)), 'id = ' . $data['id']); + } + + // Loop through the Unique fields - a little trickier due to now using field handles over feed handles + foreach ($currentData as $data) { + $newMapping = array(); + + $fieldMapping = json_decode($data['fieldMapping'], true); + $fieldUnique = json_decode($data['fieldUnique'], true); + + if ($fieldUnique) { + foreach ($fieldUnique as $feedHandle => $isUnique) { + if (isset($fieldMapping[$feedHandle])) { + $handle = $fieldMapping[$feedHandle]; + + $newMapping[$handle] = $isUnique; + } + } + } + + craft()->db->createCommand()->update('feedme_feeds', array('fieldUnique' => json_encode($newMapping)), 'id = ' . $data['id']); + } + + return true; + } +} diff --git a/feedme/migrations/m170129_000000_feedMe_changeImportStrategy.php b/feedme/migrations/m170129_000000_feedMe_changeImportStrategy.php new file mode 100644 index 00000000..bfe7923c --- /dev/null +++ b/feedme/migrations/m170129_000000_feedMe_changeImportStrategy.php @@ -0,0 +1,40 @@ +dbConnection->schema->getTable('{{feedme_feeds}}'); + + // Change the Duplication Handling column from Enum to Text + craft()->db->createCommand()->alterColumn('feedme_feeds', 'duplicateHandle', ColumnType::Text); + + // Now, as each column will have a single string value, we turn that into an array + $currentData = craft()->db->createCommand() + ->select('*') + ->from('feedme_feeds') + ->queryAll(); + + foreach ($currentData as $data) { + $duplicateHandle = array($data['duplicateHandle']); + + // But - we want to ensure backward compatibility: + // add = Add + // update = Add + Update + // delete = Add + Update + Delete + if ($data['duplicateHandle'] == 'update') { + $duplicateHandle[] = 'add'; + } + + if ($data['duplicateHandle'] == 'delete') { + $duplicateHandle[] = 'add'; + $duplicateHandle[] = 'update'; + } + + craft()->db->createCommand()->update('feedme_feeds', array('duplicateHandle' => json_encode($duplicateHandle)), 'id = ' . $data['id']); + } + + return true; + } +} diff --git a/feedme/models/FeedMe_FeedModel.php b/feedme/models/FeedMe_FeedModel.php index 10c84b3a..980f3eeb 100644 --- a/feedme/models/FeedMe_FeedModel.php +++ b/feedme/models/FeedMe_FeedModel.php @@ -3,11 +3,22 @@ class FeedMe_FeedModel extends BaseModel { - function __toString() + // Public Methods + // ========================================================================= + + public function __toString() { return Craft::t($this->name); } + public function getDuplicateHandleFriendly() + { + return FeedMeDuplicate::getFrieldly($this->duplicateHandle); + } + + // Protected Methods + // ========================================================================= + protected function defineAttributes() { return array( @@ -21,18 +32,17 @@ protected function defineAttributes() FeedMe_FeedType::JSON, )), 'primaryElement' => AttributeType::String, - 'section' => AttributeType::String, - 'entrytype' => AttributeType::String, + 'elementType' => AttributeType::String, + 'elementGroup' => AttributeType::Mixed, 'locale' => AttributeType::String, - 'duplicateHandle' => array(AttributeType::Enum, 'values' => array( - FeedMe_Duplicate::Add, - FeedMe_Duplicate::Update, - FeedMe_Duplicate::Delete, - )), - 'fieldMapping' => AttributeType::Mixed, - 'fieldUnique' => AttributeType::Mixed, - 'passkey' => AttributeType::String, - 'backup' => AttributeType::Bool, + 'duplicateHandle' => AttributeType::Mixed, + 'fieldMapping' => AttributeType::Mixed, + 'fieldDefaults' => AttributeType::Mixed, + 'fieldElementMapping' => AttributeType::Mixed, + 'fieldElementDefaults' => AttributeType::Mixed, + 'fieldUnique' => AttributeType::Mixed, + 'passkey' => AttributeType::String, + 'backup' => AttributeType::Bool, ); } } diff --git a/feedme/models/FeedMe_FeedNodeModel.php b/feedme/models/FeedMe_FeedNodeModel.php deleted file mode 100644 index 6a576fcd..00000000 --- a/feedme/models/FeedMe_FeedNodeModel.php +++ /dev/null @@ -1,18 +0,0 @@ -value; - } - - protected function defineAttributes() - { - return array( - 'value' => AttributeType::Mixed, - 'attributes' => AttributeType::Mixed, - ); - } -} diff --git a/feedme/models/FeedMe_LicenseModel.php b/feedme/models/FeedMe_LicenseModel.php new file mode 100644 index 00000000..d78f7b62 --- /dev/null +++ b/feedme/models/FeedMe_LicenseModel.php @@ -0,0 +1,40 @@ + array(AttributeType::String), + 'requestIp' => array(AttributeType::String), + 'requestTime' => array(AttributeType::String), + 'requestPort' => array(AttributeType::String), + + 'craftBuild' => array(AttributeType::String), + 'craftVersion' => array(AttributeType::String), + 'craftEdition' => array(AttributeType::String), + 'craftTrack' => array(AttributeType::String), + 'userEmail' => array(AttributeType::String), + + 'licenseKey' => array(AttributeType::String), + 'licensedEdition' => array(AttributeType::String), + 'requestProduct' => array(AttributeType::String), + 'requestVersion' => array(AttributeType::String), + 'data' => array(AttributeType::Mixed), + 'errors' => array(AttributeType::Mixed), + ); + } +} diff --git a/feedme/records/FeedMe_FeedRecord.php b/feedme/records/FeedMe_FeedRecord.php index 6afe9d7a..ff28ded5 100644 --- a/feedme/records/FeedMe_FeedRecord.php +++ b/feedme/records/FeedMe_FeedRecord.php @@ -20,18 +20,17 @@ protected function defineAttributes() FeedMe_FeedType::JSON, )), 'primaryElement' => array(AttributeType::String), - 'section' => array(AttributeType::String, 'required' => true), - 'entrytype' => array(AttributeType::String, 'required' => true), + 'elementType' => array(AttributeType::String, 'required' => true), + 'elementGroup' => array(AttributeType::Mixed), 'locale' => array(AttributeType::String), - 'duplicateHandle' => array(AttributeType::Enum, 'required' => true, 'values' => array( - FeedMe_Duplicate::Add, - FeedMe_Duplicate::Update, - FeedMe_Duplicate::Delete, - )), - 'fieldMapping' => array(AttributeType::Mixed), - 'fieldUnique' => array(AttributeType::Mixed), - 'passkey' => array(AttributeType::String, 'required' => true), - 'backup' => AttributeType::Bool, + 'duplicateHandle' => array(AttributeType::Mixed, 'required' => true), + 'fieldMapping' => AttributeType::Mixed, + 'fieldDefaults' => AttributeType::Mixed, + 'fieldElementMapping' => AttributeType::Mixed, + 'fieldElementDefaults' => AttributeType::Mixed, + 'fieldUnique' => AttributeType::Mixed, + 'passkey' => array(AttributeType::String, 'required' => true), + 'backup' => AttributeType::Bool, ); } diff --git a/feedme/records/FeedMe_LogRecord.php b/feedme/records/FeedMe_LogRecord.php deleted file mode 100644 index c5e0109b..00000000 --- a/feedme/records/FeedMe_LogRecord.php +++ /dev/null @@ -1,25 +0,0 @@ - AttributeType::Mixed, - ); - } - - public function defineRelations() - { - return array( - 'logs' => array(static::BELONGS_TO, 'FeedMe_LogsRecord'), - ); - } -} diff --git a/feedme/records/FeedMe_LogsRecord.php b/feedme/records/FeedMe_LogsRecord.php deleted file mode 100644 index ca246d16..00000000 --- a/feedme/records/FeedMe_LogsRecord.php +++ /dev/null @@ -1,26 +0,0 @@ - AttributeType::Number, - ); - } - - public function defineRelations() - { - return array( - 'feed' => array(static::BELONGS_TO, 'FeedMe_FeedRecord', 'onDelete' => static::CASCADE, 'required' => false), - 'log' => array(static::HAS_MANY, 'FeedMe_LogRecord', 'logId'), - ); - } -} diff --git a/feedme/resources/css/FeedMe.css b/feedme/resources/css/FeedMe.css index 6f09ea24..1c793959 100644 --- a/feedme/resources/css/FeedMe.css +++ b/feedme/resources/css/FeedMe.css @@ -1,47 +1,178 @@ -.duplicateHandle input.checkbox + label { - display: block; -} -span.info { - vertical-align: top; -} + +/* ========================================================================== +// Feeds Index +// ========================================================================== */ .fa { vertical-align: middle; } -.fa-icon.direct .fa-external-link { +.fa-icon.direct .fa-external-link, +.fa-icon.debug .fa-bug { color: rgba(0, 0, 0, 0.2); } -.fa-icon.direct:hover .fa-external-link { +.fa-icon.direct:hover .fa-external-link, +.fa-icon.debug:hover .fa-bug { color: #0d78f2; } -body.ltr table.data td:last-child { +.fa-icon.debug .fa-bug { + +} + +body.ltr #feeds .thin.action { padding-left: 10px; } -#plugin-footer { - margin: 24px 0 0 0; - width: 100% +table#feeds .element-group-sub { + display: block; + font-size: 11px; + line-height: 1.2; + color: rgba(51, 63, 77, 0.5); + font-weight: bold; } -#plugin-footer .footer-left { + + + +/* ========================================================================== +// Field-Mapping Step +// ========================================================================== */ + +.feedme-mapping th:nth-child(1) { + width: 30%; +} + +.feedme-mapping th:nth-child(2) { + width: 40%; +} + +.feedme-mapping th:nth-child(3) { + width: 30%; +} + +.feedme-mapping td.col-map .field .heading { + font-size: 11px; +} + +.feedme-mapping td.col-map .field { + margin: 10px 0; +} + +.feedme-mapping .field-extra-settings { + font-size: 11px; +} + +.feedme-mapping .field-extra-settings input.checkbox + label:before, +.feedme-mapping .field-extra-settings div.checkbox:before { + top: 0; +} + +.feedme-mapping .field-extra-settings .assets-uploads .checkboxfield { float: left; + margin-right: 10px; + padding-top: 2px; + margin-bottom: 0; + z-index: 1; } -#plugin-footer .footer-right { - float: right; +.feedme-mapping .field-extra-settings .assets-uploads .select { + opacity: 0; + visibility: hidden; } -#plugin-footer .plugin-credit { +.feedme-mapping .field-extra-settings .assets-uploads select { + padding: 5px 22px 5px 10px !important; font-size: 11px; - color: #8f98a3; - text-align: right; - margin-top: 10px; + line-height: 11px; +} + +.feedme-mapping .field-extra-settings .element-match span { + float: left; + margin-right: 10px; + padding-top: 2px; + margin-bottom: 0; + z-index: 1; +} + +.feedme-mapping .field-extra-settings .element-match select { + padding: 5px 22px 5px 10px !important; + font-size: 11px; + line-height: 11px; +} + +.feedme-mapping .element-sub-field td { + background: #F7F7F8; +} + +.feedme-mapping .element-sub-field .col-field { + padding-left: 10px; +} + +.feedme-mapping .element-sub-field .col-default { + padding-right: 10px; +} + + + + +/* ========================================================================== +// Full Page Screens for Error/Success +// ========================================================================== */ + +.feedme-fullpage { + text-align: center; + max-width: 40em; + margin: 24px auto 48px; +} + +.feedme-fullpage h2 { + font-size: 18px; +} + +.feedme-fullpage img { + width: 70px; +} + +.feedme-fullpage .buttons { + display: inline-block; + margin-top: 15px; +} + +.feedme-fullpage .progress-container { + display: inline-block; + margin-top: 15px; +} + + + +/* ========================================================================== +// Direct Feed Additional Styles +// ========================================================================== */ + +body.feedme-message .message-container .pane { + width: 36em; + padding: 2em; + margin: 0 auto !important; + + -ms-transform: translate(0, -50%); + -webkit-transform: translate(0, -50%); + transform: translate(0, -50%); } +body.feedme-message .feedme-fullpage { + margin-bottom: 24px; +} + + + + +/* ========================================================================== +// Help Form +// ========================================================================== */ + .feedme-help-form { width: 320px; margin-bottom: 40px; @@ -78,3 +209,117 @@ body.ltr table.data td:last-child { font-size: 12px; color: #8f98a3; } + + + +/* ========================================================================== +// Plugin License +// ========================================================================== */ + +#loading-status { display: inline-block; padding: 6px 0; } + +.reg-header { position: relative; min-height: 150px; box-sizing: border-box; padding-top: 50px; } +body.ltr .reg-header { padding-left: 150px; padding-right: 25px; } +body.rtl .reg-header { padding-right: 150px; padding-left: 25px; } +#unknown-license-header { padding-top: 42px; } + +.reg-header img { position: absolute; top: 25px; } +body.ltr .reg-header img { left: 25px; } +body.rtl .reg-header img { right: 25px; } + +.reg-header h2 { margin: 0; font-size: 20px; } +.reg-header p { margin-top: 0.5em; max-width: 515px; } + +.license-meta { margin: 50px 0 25px; } +.license-meta > .data { padding: 0 !important; } +.license-meta > .data .heading { width: 125px; } +body.ltr .license-meta > .data .heading { text-align: right; } +body.rtl .license-meta > .data .heading { text-align: left; } +.license-meta > .data .value { width: calc(100% - 150px); } + +body.ltr .indented { margin-left: 150px; } +body.rtl .indented { margin-right: 150px; } + +#license-key-wrapper { display: inline-block; } + +@media only screen and (max-width: 768px) { + .reg-header { min-height: 0; padding: 0 !important; text-align: center; } + .reg-header img { position: static; margin-bottom: 14px; } + + .meta > .data { display: block; } + .license-meta > .data .heading, + .license-meta > .data .value { width: auto; } + body.ltr .license-meta > .data .heading { text-align: left; } + body.rtl .license-meta > .data .heading { text-align: right; } + + #license-key-wrapper { display: block; margin-bottom: 24px; } + #license-key-wrapper input { width: 100%; } + + body.ltr .indented { margin-left: 0; } + body.rtl .indented { margin-right: 0; } +} + + + +/* ========================================================================== +// Welcome Screen +// ========================================================================== */ + +.feedme-welcome { + text-align: center; + margin: 0 auto; +} + +.feedme-welcome h1 { + font-size: 28px; +} + +.feedme-welcome img { + width: 120px; + margin: 20px 0 30px; +} + +.feedme-welcome .btn-start { + font-size: 15px; + padding: 14px 28px; + height: auto; + margin-top: 20px; +} + +.feedme-welcome .or { + display: block; + padding: 10px; + font-style: italic; +} + +.feedme-welcome .btn .fa { + margin-left: 5px; +} + + + + +/* ========================================================================== +// Plugin Footer +// ========================================================================== */ + +#plugin-footer { + margin: 24px 0 0 0; + width: 100%; +} + +#plugin-footer .footer-left { + float: left; +} + +#plugin-footer .footer-right { + float: right; +} + +#plugin-footer .plugin-credit { + font-size: 11px; + color: #8f98a3; + text-align: right; + margin-top: 10px; +} + diff --git a/feedme/resources/icon-error.svg b/feedme/resources/icon-error.svg new file mode 100755 index 00000000..f6105c13 --- /dev/null +++ b/feedme/resources/icon-error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/feedme/resources/icon-license-invalid.svg b/feedme/resources/icon-license-invalid.svg new file mode 100644 index 00000000..98f97920 --- /dev/null +++ b/feedme/resources/icon-license-invalid.svg @@ -0,0 +1,42 @@ + + + + +plugin-icons +Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feedme/resources/icon-license-registered.svg b/feedme/resources/icon-license-registered.svg new file mode 100644 index 00000000..61d2e3cf --- /dev/null +++ b/feedme/resources/icon-license-registered.svg @@ -0,0 +1,42 @@ + + + + +registered-icon +Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/feedme/resources/icon-success.svg b/feedme/resources/icon-success.svg new file mode 100755 index 00000000..2301da3d --- /dev/null +++ b/feedme/resources/icon-success.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/feedme/resources/icon.svg b/feedme/resources/icon.svg index d91150a0..069c57f6 100644 --- a/feedme/resources/icon.svg +++ b/feedme/resources/icon.svg @@ -1 +1,20 @@ - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/feedme/resources/js/FeedMe.js b/feedme/resources/js/FeedMe.js index 17047428..b511fb2b 100644 --- a/feedme/resources/js/FeedMe.js +++ b/feedme/resources/js/FeedMe.js @@ -1,15 +1,41 @@ $(function() { - // Find entry types by chosen section - $(document).on('change', '#section', function() { - $('#entrytype').html(''); - Craft.postActionRequest('feedMe/getEntryTypes', { 'section': $(this).val() }, function(entrytypes) { - $.each(entrytypes, function(index, value) { - $('#entrytype').append(''); - }); + // Toggle various field when changing element type + $(document).on('change', '#elementType', function() { + $('.element-select').hide(); + $('.element-select-' + $(this).val()).show(); + }) + + $('#elementType').trigger('change'); + + // Toggle the Entry Type field when changing the section select + $(document).on('change', '.element-parent-group select', function() { + var sections = $(this).parents('.element-sub-group').data('items'); + var entryType = 'item_' + $(this).val(); + var entryTypes = sections[entryType]; + + var currentValue = $('.element-child-group select').val(); + + var newOptions = ''; + $.each(entryTypes, function(index, value) { + if (index) { + newOptions += ''; + } }); + + $('.element-child-group select').html(newOptions); + + // Select the first non-empty, or pre-selected + if (currentValue) { + $('.element-child-group select').val(currentValue); + } else { + $($('.element-child-group select').children()[1]).attr('selected', true); + } }); + $('.element-parent-group select').trigger('change'); + + // Update the Primary XML Element - if not already set $(document).on('change', '#feedType', function() { // Don't do this if edit the field... @@ -28,22 +54,197 @@ $(function() { // Add an attribute to the Primary XML Element when typed in manually. This helps to prevent the above // triggering disrupt what the user has manually entered - $( "#primaryElement" ).keypress(function() { - $(this).attr('data-manual'); + $('#primaryElement').keypress(function() { + $(this).data('manual'); }); + // For field-mapping, auto-select Title if no unique checkboxes are set + if ($('.feedme-uniques').length) { + var checked = $('.feedme-uniques input[type="checkbox"]:checked').length; + + if (!checked) { + $('.feedme-uniques input[type="checkbox"]:first').prop('checked', true); + } + } - // Allow multiple back-end action hooks depending on button clicked. I'm sure there is a better way though! - $(document).on('click', 'input[type="submit"]', function(e) { - e.preventDefault(); - var form = $(this).parents('form'); + // For Assets, only show the upload options if we decide to upload + $('.assets-uploads input').on('change', function(e) { + var $options = $(this).parents('.field-extra-settings').find('.select'); - if ($(this).attr('data-action')) { - $(form).find('input[name="action"]').val($(this).attr('data-action')); + if ($(this).prop('checked')) { + $options.css({ opacity: 1, visibility: 'visible' }); + } else { + $options.css({ opacity: 0, visibility: 'hidden' }); } + }); + + // On-load, hide/show upload options + $('.assets-uploads input').trigger('change'); + + + // Allow multiple submit actions, that trigger different actions as required + $(document).on('click', 'input[data-action]', function(e) { + var $form = $(this).parents('form'); + var action = $(this).data('action'); - $(form).submit(); + $form.find('input[name="action"]').val(action); + $form.submit(); }); - -}); \ No newline at end of file + // A nice loading animation on the success page for feeds + new Craft.FeedMeTaskProgress(); + +}); + + +(function() { + +Craft.FeedMeTaskProgress = Garnish.Base.extend({ + tasksById: null, + completedTasks: null, + updateTasksTimeout: null, + + completed: false, + + init: function() { + this.tasksById = {}; + this.completedTasks = []; + + // Force the tasks icon to run + setTimeout($.proxy(function() { + this.updateTasks(); + }, this), 1000); + + //Craft.cp.stopTrackingTaskProgress(); + }, + + updateTasks: function() { + this.completed = false; + + Craft.postActionRequest('tasks/getTaskInfo', $.proxy(function(taskInfo, textStatus) { + if (textStatus == 'success') { + this.showTaskInfo(taskInfo[0]); + } + }, this)) + }, + + showTaskInfo: function(taskInfo) { + // First remove any tasks that have completed + var newTaskIds = []; + + if (taskInfo) { + newTaskIds.push(taskInfo.id); + } else { + // Likely too fast for Craft to register this was even a task! + $('.progress-container').html('
' + Craft.t('Processing complete!') + '
'); + } + + for (var id in this.tasksById) { + if (!Craft.inArray(id, newTaskIds)) { + this.tasksById[id].complete(); + this.completedTasks.push(this.tasksById[id]); + delete this.tasksById[id]; + } + } + + // Now display the tasks that are still around + if (taskInfo) { + var anyTasksRunning = false, + anyTasksFailed = false; + + if (!anyTasksRunning && taskInfo.status == 'running') { + anyTasksRunning = true; + } else if (!anyTasksFailed && taskInfo.status == 'error') { + anyTasksFailed = true; + } + + if (this.tasksById[taskInfo.id]) { + this.tasksById[taskInfo.id].updateStatus(taskInfo); + } else { + this.tasksById[taskInfo.id] = new Craft.FeedMeTaskProgress.Task(taskInfo); + } + + if (anyTasksRunning) { + this.updateTasksTimeout = setTimeout($.proxy(this, 'updateTasks'), 500); + } else { + this.completed = true; + + if (anyTasksFailed) { + //Craft.cp.setRunningTaskInfo({ status: 'error' }); + } + } + } else { + this.completed = true; + //Craft.cp.setRunningTaskInfo(null); + } + } +}); + +Craft.FeedMeTaskProgress.Task = Garnish.Base.extend({ + id: null, + level: null, + description: null, + + status: null, + progress: null, + + $container: null, + $statusContainer: null, + $descriptionContainer: null, + + _progressBar: null, + + init: function(info) { + this.id = info.id; + this.level = info.level; + this.description = info.description; + + this.$container = $('.progress-container').html($('
')); + this.$statusContainer = $('
').appendTo(this.$container); + + this.$container.data('task', this); + + this.updateStatus(info); + }, + + updateStatus: function(info) { + if (this.status != info.status) { + this.$statusContainer.empty(); + this.status = info.status; + + switch (this.status) { + case 'running': { + this._progressBar = new Craft.ProgressBar(this.$statusContainer); + this._progressBar.showProgressBar(); + break; + } + case 'error': { + $('
' + Craft.t('Processing failed. View logs') + '
').appendTo(this.$statusContainer); + break; + } + } + } + + if (this.status == 'running') { + this._progressBar.setProgressPercentage(info.progress*100); + + if (this.level == 0) { + // Update the task icon + //Craft.cp.setRunningTaskInfo(info, true); + } + } + }, + + complete: function() + { + this.$statusContainer.empty(); + $('
' + Craft.t('Processing complete!') + '
').appendTo(this.$statusContainer); + }, + + destroy: function() { + this.$container.remove(); + this.base(); + } +}); + +})(); \ No newline at end of file diff --git a/feedme/resources/js/FeedMeHelp.js b/feedme/resources/js/FeedMeHelp.js index 91fe1d47..a3a3f04f 100644 --- a/feedme/resources/js/FeedMeHelp.js +++ b/feedme/resources/js/FeedMeHelp.js @@ -17,6 +17,10 @@ Craft.FeedMeHelp = Garnish.Base.extend({ $iframe: null, init: function() { + this.widgetId = 'feedMeHelp'; + + Craft.FeedMeHelp.widgets[this.widgetId] = this; + this.$widget = $('.feedme-help-form'); this.$message = this.$widget.find('.message:first'); this.$fromEmail = this.$widget.find('.fromEmail:first'); @@ -26,20 +30,29 @@ Craft.FeedMeHelp = Garnish.Base.extend({ this.$spinner = this.$widget.find('.buttons .spinner'); this.$error = this.$widget.find('.error:first'); this.$form = this.$widget.find('form:first'); + this.$form.prepend(''); + this.$form.prepend(Craft.getCsrfInput()); this.addListener(this.$sendBtn, 'activate', 'sendMessage'); }, sendMessage: function() { + var iframeName = 'iframeWidget' + this.widgetId; + if (this.loading) return; + if (!this.$iframe) { + this.$iframe = $('