first import

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-08 11:40:19 +02:00
commit 1bc61b12ad
8435 changed files with 1582817 additions and 0 deletions

View File

@@ -0,0 +1,541 @@
Feeds 7.x 2.0 Alpha 5, 2012-05-28
---------------------------------
- Issue #1515204 by gnucifer: Malformed destination uri in FeedsEnclosure.
- Issue #1450714 by getgood: ATOM parser ignores 'updated' tag
- Issue #1152940 by bblake, rickmanelius, g089h515r806, darrylri, iMiksu,
sdrycroft, johnbarclay, batje, axel.rutz, GaëlG: Feeds term import with
hierarchy and weight
- Issue #1406260 by Xen, logaritmisk: Fetchers without source configuration
fails.
- Issue #1407670 by Ivan Simonov, GaëlG, franz: drupal_strlen() doesn't work
when parsing multibyte strings in CSVParser.
- Issue #1213472 by paulgemini, Nigel_S, emarchak, Jorenm: Fixed Unsupported
operand types in FeedsConfigurable.
- Follow-up to #1245094 by agoradesign: menu links not really fixed.
- Issue #1245094 by chrisdejager, dman: Fixed Node menu link deleted on update.
- Issue #712304 by derhasi, twistor, jerdavis, alex_b, rjbrown99, rbayliss |
ManyNancy: Fixed Batch import does not continue where it left off, instead
starts from the beginning.
- Issue #959984 by kidrobot, desmondmorris, slashrsm, flailingmaster, rfay |
dasjo: Fixed taxonomy_node_get_terms() doesn't work with drupal 7.
- Issue #1140194 by orb | emorency: Fixed SQLSTATE[HY000]: General error: 1366
Incorrect string value for a field with accents.
- Issue #1427642 by elliotttf: Fixed Use drupal_exit() rather than exit() in
PuSH/FeedsHTTPFetcher callbacks.
- Issue #1418382: Fixed unsubscribe requests to PuSH hubs failing due to
transaction in node_delete().
- Issue #870556: Fixed PuSH verifications run before the subscription records
have been saved."
- Issue #1289598 by emackn, andypost: Fixed Remove check_plain() on form
options.
- Issue #996808 by twistor, marcvangend, joshuajabbour: Fixed Update existing
doesn't reset targets that have real_target() set.
- Issue #1219180 by dandaman: Fixed Minor error in API code samples.
- Issue #686470 by johnv, wojtha, febbraro | rjbrown99: Fixed Filefield mapper:
URLs with spaces not parsed properly.
- Issue #1380636 by gnucifer, emackn: Fixed Wrong response headers reported on
3xx requests in function http_request_get().
- Issue #1382208 by slcp: Fixed FeedsSource.inc sourceSave() and sourceDelete()
function descriptions are the wrong way round.
- Issue #1372074 by twistor, emackn: Fixed feeds_http_request() does not cache
when using drupal_http_request().
- Issue #1191330 by twistor: Added Allow feeds_mapper().test to set field
instance settings.
- Issue #1225672 by StepanKuzmin, tekante | guillaumev: Fixed Bug when importing
a single year.
- Issue #1035684 by mikejoconnor: Source / Target sort order
- Issue #1005128 by dasjo, fago: Rules integration & enable modules to customize
imports.
- Issue #1228568 by pcambra: Add user status to FeedsUserProcessor
- Issue #1139676 by Dave Reid, cosmicdreams: Fixed feeds_alter() doesn't support
modulename.feeds.inc files supported by feeds_hook_info().
- Issue #1347894 by juampy | colin_young: Fixed Clear cache causes integrity
constraint violation. Fix for feeds info file
- Issue #1298326 by twistor, Dave Reid, and emackn: Fixed Only execute
rebuild_menu() when necessary.
- Issue #1228570 by pcambra: Added user language to FeedsUserProcessor.
- Issue #1206042 by anon: Let node titles have unique option.
- Issue #126689 by tcindie :Additional targets for nodes
- issue #126689 by tcindie : 1298326Issue PageOnly execute rebuild_menu when
necessary
- Issue #1128418 by mongolito404, Niklas Fiekas, jief: Fixed Deprecated:
Assigning the return value of new by reference is deprecated in
feeds_include_library().
- Issue #1046916 by eosrei: importing nodes with their original NID
- Issue #1197646: Skip importer config form validation if a machine name was not
provided.
- Issue #1248712: Show an empty row result and hide the Save button if no
importers are available.
- Issue #1248710: Use the core-provided machine_name FAPI element for the
importer machine name field.
- Issue #1235394: Fixed menu paths violate UX standards and could not use
breadcrumbs.
- Issue #1248648 by twistor, Dave Reid: Fixed bugs and inconsistencies in
FeedsRSStoNodesTest.
Feeds 7.x 2.0 Alpha 4, 2011-06-28
---------------------------------
- Issue #1161810: Fixed declaration of FeedsSource::instance() should be
compatible with that of FeedsConfigurable::instance().
- Issue #1201638: Plugins should be listed in feeds.info as files[] records for
the class registry.
- Issue #1149226: Fixed invalid message parameter passed into feeds_log from
FeedsProcessor::process().
- Issue #1044882 by rfay, Dave Reid: Fixed indexes for {feeds_item} are too long
and can cause problems during install or uninstall.
- Fixing PHP strict error in _parser_common_syndication_atom10_parse().
- Added support for feeds hooks to be located in modulename.feeds.inc.
- Issue #1191554: Fixed failures in FeedsUIUserInterfaceTestCase.
- Issue #1191564: Use FeedsWebTestCase for FeedsDateTimeTest.
- Issue #1191494 by twistor, Dave Reid: Fixed link to node type feed importer
did not use node_access().
- Issue #1191450: Fixed mismatch of arguments for t().
- Fixed possible XSS with field labels in Feed importer mapping settings.
- Fixed coder violations and standards.
- Issue #723548: Added unit tests for feeds_valid_url().
- Fixed PHP notice with undefined variables in
http_request_get_common_syndication().
- Issue #723548: Added support for feed URLs with feed:// and webcal://.
- Fixed error when calling form_set_error() and title field on follow-up to fix
feed node title fields not actually un-required.
- Issue #1191210: Added feeds_field_extra_fields() so the 'Feed' fieldset can be
re-ordered through the Field UI.
- Issue #1191194: Fixed test failure in FeedsCSVtoUsersTest due to lack of
'administer users' permission.
- Issue #1191200: Fixed display of the description field on the feed item
content type.
- Issue #1066286: Added test to ensure 'Feed items' doesn't display on non-feed
nodes.
- Use hook_form_node_form_alter() rather than hook_form_alter().
- Simplify FeedsMapperTestCase::createContentType() using
DrupalWebTestCase::drupalCreateContentType().
- Issue #983220 by twistor: Fixed field mapper tests failed due to number.module
not being enabled.
- Issue #1085092 by pfrenssen: Fixed module_list() is called twice in
feeds_alter().
- Fixes and cleanups to tests.
- Use fetchObject() rather than fetch() since it is more explicit.
- Issue #1008384 by twistor: Fixed feeds not pulling publication date with feed
using dc:date and RSS 2.0.
- Issue #769084: Fixed use of isset() rather than !empty() causes import
problems with _parser_common_syndication_RSS20_parse().
- Issue #914210 by jyee, Dave Reid: Added mapper for user raw password.
- Issue #974494: Fixed PHP notice 'Undefined property: stdClass::$openid in
FeedsUserProcessor->entitySave()'.
- Issue #1134684 by rfay, Dave Reid: Fixed improper parameters for
file_field_widget_uri().
- Issue #1055582: Fixed strict notice that FeedsDateTime::setTimezone() is not
compatible with DateTime::setTimezone().
- Issue #1085194: Not all selected mappings are removed.
- Issue #1032640: Added basic token integration.
- Issue #1066806: Use hook_entity_insert/update/delete rather than separate
node, taxonomy term, and user hooks.
- Issue #1066822: Fixed bugs and inconsistencies with test files and getInfo()
declarations.
- Issue #1066810: Fixed list of 'Expire nodes' options in FeedsNodeProcessor.
- Issue #1066286: Fixed 'View items' tab added to all content types.
- Issue #1011958 by David Goode: Allow hashes to be updated when content in a 
feed is updated.
- Issue #1048642 by greg.harvey: Check for remove_flags in Feeds UI before using 
that variable.
- #967018 jcarlson34, David Goode, alex_b: Mapping to String lists not supported
Feeds 7.x 2.0 Alpha 3, 2011-01-14
---------------------------------
- Add index for looking up by entity_type + url/ guid to feeds_item table.
- #994026 tristanoneil: Optionally defuse email addresses.
Feeds 7.x 2.0 Alpha 2, 2010-11-02
---------------------------------
- #940866 tristanoneil: PHP 5.3 FeedsImporter::copy function must be compatible.
- #944986 tristanoneil: Link Mapper Upgrade.
- #959066 tristanoneil: Remove old mappers and tests.
- #883342 Steven Jones: Don't force usage of cURL.
- #776854 imclean et. al.: Support parsing CSV files without column headers.
- Ian Ward: ensure that arrays of numerics are handled correctly.
- #953728 tristanoneil: Upgrade text formats, use on all processors.
- alex_b: Fix file mapper, add file mapper tests, generate flickr.xml and
files.csv dynamically.
- #953538 yhahn: Remove BOM from UTF-8 files.
Adds sanitizeFile() and sanitizeRaw() methods to FeedsFetcherResult.
Extending classes that override either the getRaw() or getFilePath() methods
should call these sanitization methods to ensure that the returned output /
file has been cleaned for parsing.
- #606612 alex_b: More detailed log.
- #949236 Ian Ward, alex_b: Allow mapping empty values to fields.
- #912630 twistor, alex_b: FeedsParserResult: make items accessible for
modification.
- #933306 alex_b: Fix Feeds creates subscriptions for not existing importers.
- #946822 twistor: FeedsSitemapParser broken: Serialization of
'SimpleXMLElement' is not allowed.
- #949916 alex_b: Convert values mapped to user->created.
- #949236 Ian Ward: Allow mapping empty values to fields.
- Allow for mapping to list_number field types.
- Fix entity inspection in file fetcher.
- #932772 alex_b: FeedsProcessor: Consolidate process() and clear().
FeedsProcessor now implements the process() and clear() methods for creating
and deleting entities. The extending processors FeedsNodeProcessor,
FeedsTermProcessor and FeedsUserProcessor merely implement entity manipulation
methods (newEntity(), entityLoad(), entitySave(), entityDeleteMultiple()...).
This brings features previously only available on FeedsNodeProcessor to all
entity processors: fast change detection on imported items with hashes,
batching on process() and clear() and in the case of FeedsUserProcessor, the
actual implementation of clear(). Together with #929066 this is a further
step towards harmonizing features of processor plugins.
- Move term and user validation into a validate() method.
- Remove check for present name and mail. Needs to be solved on a more pluggable
level.
- FeedsTermProcessor: Do not filter taxonomy_term_data table by vid when
clearing.
- #932572 alex_b: FeedsTermProcessor: Batch term processing.
- Remove check for present name in terms that are imported. If we do such
validation, we need to do this on a more pluggable level.
- Fix Feeds News tests, add a 'description' field to the Feeds Item content
type.
- #728534 alex_b: Remove FeedsFeedNodeProcessor. If you have used
FeedsFeedNodeProcessor in the past, use FeedsNodeProcessor (Node Processor)
instead now. It supports all of FeedsFeedNodeProcessor's functionality and
more.
- #929066 alex_b: Track all imported items. Note: All views that use 'Feeds
Item' fields or relationships need updating.
- #930018 alex_b: Don't show file upload when 'Supply path directly' is
selected.
- #927892 alex_b: Add "Process in background" feature. Allows one-off imports to
be processed in the background rather than using Batch API. Useful for very
large imports.
- #929058 alex_b: Report status of source on import and delete forms, track
last updated time on a source level.
- #928836: Set progress floating point directly. Note: fetchers and parsers
must use $state->progress() for setting the batch progress now IF they support
batching.
- #928728: Track source states by stage, not by plugin. Note: call signature of
FeedsSource::state() has changed.
- Remove 6.x upgrade hooks.
- #923318: Fix Fatal error: Call to a member function import() on a non-object
occuring on cron.
- Clean up basic settings form.
- Make getConfig() include configuration defaults.
Feeds 7.x 2.0 Alpha 1, 2010-09-29
---------------------------------
- #925842 alex_b: Support batching through directories on disk.
- #625196 mstrelan, alex_b: Fix array_merge(), array_intersect_key() warnings.
- Remove hidden setting feeds_worker_time. Use hook_cron_queue_info_alter() to
modify this setting.
- #744660-80 alex_b: Expand batch support to fetchers and parsers.
- Removed FeedsBatch classes in favor of FeedsResult classes.
- Variable 'feeds_node_batch_size' is now called 'feeds_process_limit'.
- Signature of FeedsParser::getSourceElement() changed.
- Signature of FeedsProcessor::uniqueTargets() changed.
- Signature of FeedsProcessor::existingItemId() changed.
- Sigature for callbacks registered by hook_feeds_parser_sources_alter()
changed.
- Return value of FeedsFetcher::fetch() changed.
- Signature and return value of FeedsParser::parse() changed.
- Signature of FeedsProcessor::process() changed.
- Signature of hook_feeds_after_parse() changed.
- Signature of hook_feeds_after_import() changed.
- Signature of hook_feeds_after_clear() changed.
Feeds 7.x 1.0 Alpha 1, 2010-09-21
---------------------------------
Equal to http://github.com/lxbarth/Feeds/commits/DRUPAL-7--1-0-alpha1
- Expire files returned by FeedsImportBatch after DRUPAL_MAXIMUM_TEMP_FILE_AGE
seconds.
- FeedsFileFetcher: track uploaded files, delete unused files.
- yhahn: Upgrade FeedsTaxonomyProcessor.
- Remove handling of target items that are array. All target items must be
objects now.
- Upgrade file and image mapper.
- Upgrade taxonomy mapper.
- Upgrade field mapper.
- Move plugin handling into FeedsPlugin class.
- Base level upgrade.
Feeds 6.x 1.0 Beta 6, 2010-09-16
--------------------------------
- #623432 Alex UA, dixon_, pvhee, cglusky, alex_b et al.: Mapper for emfield.
- #913672 andrewlevine: Break out CSV Parser into submethods so it is more
easily overridable
- #853974 snoldak924, alex_b: Fix XSS vulnerabilities in module.
- #887846 ekes: Make FeedsSimplePieEnclosure (un)serialization safe.
- #908582 XiaN Vizjereij, alex_b: Fix "Cannot use object of type stdClass as
array" error in mappers/taxonomy.inc.
- #906654 alex_b: Fix phantom subscriptions.
- #867892 alex_b: PubSubHubbub - slow down import frequency of feeds that are
subscribed to hub.
- #908964 alex_b: Break out scheduler. Note: Features depends on Job Scheduler
module now: http://drupal.org/project/job_scheduler
- #663860 funkmasterjones, infojunkie, alex_b et. al.: hook_feeds_after_parse().
- #755556 Monkey Master, andrewlevine, alex_b: Support saving local files in
filefields.
- #891982 bangpound, twistor: Support Link 2.x.
- #870278 budda: Fix SQL query in taxonomy_get_term_by_name_vid().
- #795114 budda, alex_b: Taxonomy term processor doesn't require vocabulary to
be set.
Feeds 6.x 1.0 Beta 5, 2010-09-10
--------------------------------
- #849840 adityakg, rbayliss, alex_b: Submit full mapping on every submission.
- #849834 rbayliss, alex_b: Generalize feeds_config_form() to feeds_form().
- #907064 alex_b: Track imported terms.
- #906720 alex_b: Introduce a hook_feeds_after_clear().
- #905820 tristan.oneil: Adjust delete message in FeedsDataProcessor to avoid
misleading total numbers.
- #671538 mburak: Use CURLOPT_TIMEOUT to limit download time of feeds.
- #878002 Will White, David Goode: Support multiple sources per mapping target
in FeedsDataProcessor.
- #904804 alex_b: Support exportable vocabularies.
- #836876 rsoden, Will White, alex_b: Add simple georss support to Common
Syndication Parser.
- #889196 David Goode: Support for non-numeric vocabulary IDs for feature-based
vocabularies.
- #632920 nickbits, dixon_, David Goode, alex_b et al: Inherit OG, taxonomy,
language, user properties from parent feed node. Note: Signatures of
FeedsProcessor::map(), existingItemId(), FeedsParser::getSourceElement()
changed.
- #897258 TrevorBradley, alex_b: Mapping target nid.
- #873198 BWPanda, morningtime: Import multiple values to tag vocabulary.
- #872772 andrewlevine: Fix buildNode() (and node_load()) called unnecessarily.
- #873240 thsutton: Use isset() to avoid notices.
- #878528 Sutharsan: Don't show file in UI if file does not exist.
- #901798 alex_b: Fix time off in SitemapParser.
- #885724 eliotttf: Avoid array_flip() on non scalars.
- #885052 Hanno: Fix small typo in access rights.
- #863494 ekes, alex_b: Delete temporary enclosures file.
- #851194 alex_b: Featurize - move default functionality from feeds_defaults to
feeds_fast_news, feeds_import and feeds_news features.
- #617486 alex_b: Create link to original source, view of items on feed nodes.
- #849986 lyricnz, alex_b: Cleaner batch support.
- #866492 lyricnz: Clean up tests.
- #862444 pounard: Do not name files after their enclosure class.
- #851570 morningtime: Avoid trailing slashes when passing file paths to
file_check_directory().
- #836090 andrewlevine, alex_b: Include mapping configuration in hash.
- #853156 alex_b: Support real updates of terms.
- #858684 alex_b: Fix notices when file not found.
Feeds 6.x 1.0 Beta 4, 2010-07-25
--------------------------------
- #838018-12 Remove Formatted Number CCK mapper, cannot be properly tested, see
#857928.
Feeds 6.x 1.0 Beta 3, 2010-07-18
--------------------------------
- #854628 DanielJohnston, alex_b: Fix user processor assigns all roles.
- #838018 infojunkie: Mapper for Formatted Number CCK field.
- #856408 c.ex: Pass all $targets for hook_feeds_node_processor_targets_alter()
by reference.
- #853194 andrewlevine, alex_b: Mapping: don't reset all targets.
- #853144 alex_b: Consistent use of "replace" vs "update".
- #850998 alex_b: Clean up file upload form. Note: If you supply file paths
directly in the textfield rather than uploading them through the UI, you will
have to adjust your importer's File Fetcher settings.
- #850652 alex_b: Make ParserCSV (instead of FeedsCSVParser) populate column
names.
- #850638 alex_b: Introduce FeedsSource::preview().
- #850298 alex_b: ParserCSV: Support batching (only affects library, full parser
level batch support to be added later with #744660).
- Minor cleanup of admin UI language and CSS.
- #647222 cglusky, jeffschuler: Specify input format for feed items.
Feeds 6.x 1.0 Beta 2, 2010-07-10
--------------------------------
- #753426 Monkey Master, andrewlevine, alex_b: Partial update of nodes.
- #840626 andrewlevine, alex_b: Support using same mapping target multiple
times.
- #624464 lyricnz, alex_b: Fix to "support tabs as delimiters".
- #840350 lyricnz: (Optionally) Transliterate enclosure filenames to provide
protection from awkward names.
- #842040 dixon_: Accept all responses from the 2xx status code series.
- #836982 Steven Merrill: Fix Feeds.module tests do not work when run from the
command line.
Feeds 6.x 1.0 Beta 1, 2010-06-23
--------------------------------
Feeds 6.x 1.0 Alpha 16, 2010-06-19
----------------------------------
- #830438 andrewlevine: More secret files in FeedsImportBatch::getFilePath().
- #759302 rjb, smartinm, et. al: Fix user warning: Duplicate entry.
- #819876 alex_b: Fix field 'url' and 'guid' don't have default values.
- #623444 mongolito404, pvhee, pdrake, servantleader, alex_b et. al.: Mapper for
link module.
- #652180 ronald_istos, rjbrown99, et. al.: Assign author of imported nodes.
- #783098 elliotttf: Introduce hook_feeds_user_processor_targets_alter(), mapper
for user profile fields.
Feeds 6.x 1.0 Alpha 15, 2010-05-16
----------------------------------
- #791296 B-Prod: Fix Feeds data processor does update id 0.
- #759904 lyricnz: Provide a Google Sitemap Parser.
- #774858 rjbrown99: Fix Node Processor updates node "created" time when
updating.
- #704236 jerdavis: Support mapping to CCK float field.
- #783820 klonos: Fix warning: copy() [function.copy]: Filename cannot be empty
in FeedsParser.inc on line 168.
- #778416 clemens.tolboom: Better message when plugin is missing.
- #760140 lyricnz: FeedsBatch->total not updated when addItem($item) is called.
- #755544 Monkey Master: Keep batch processing when mapping fails.
- alex_b: Reset import schedule after deleting items from feed.
- #653412 rbrandon: Do not create items older than expiry time.
- #725392 nicholasThompson: FeedsBatch does not check feeds folder exists before
uploading.
- #776972 lyricnz: Messages use plural when describing single item.
- #701390 frega, morningtime, Mixologic, alex_b et. al.: Fix RSS 1.0 parsing
and add basic test framework for common_syndication_parser.
- #781058 blakehall: Create teaser for imported nodes. NOTE: this may mean that
your existing installation has shorter node teasers as expected. If this is
the case, increase "Length of trimmed posts" on admin/content/node-settings.
- #622932-30 mikl: fix remaining non-standard SQL.
- #624464 bangpound: Support tabs as delimiters.
Feeds 6.x 1.0 Alpha 14, 2010-04-11
----------------------------------
- #758664: Fix regression introduced with #740962.
Feeds 6.x 1.0 Alpha 13, 2010-03-30
----------------------------------
- #622932 pounard: Fix SQL capitalization.
- #622932 pounard: Fix non-standard SQL (PostgreSQL compatibility)
- #705872 Scott Reynolds: Added HTTPFetcher autodiscovery
- #740962 alex_b: Fix FileFetcher Attached to Feed Node, Upload Field Not Saving
File Path.
- #754938 Monkey Master: FeedsCSVParser.inc uses strtolower() while parsing
UTF-8 files.
- #736684 Souvent22, Mixologic: FeedsDateTime & Batch DateTime causes core
dumps.
- #750168 jtr: _parser_common_syndication_title does not strip html tags before
constructing a title.
- #648080 pvhee: FeedsNodeProcessor - static caching of mapping targets makes
mapping fail with multiple feed configurations.
- #735444 Doug Preble: PubSubHubbub - Fix "Subscription refused by callback URL"
with PHP 5.2.0.
- alex_b: Suppress namespace warnings when parsing feeds for subscription in
PuSHSubscriber.inc
- #724184 ekes: catch failures when parsing for PubSubHubbub hub and self.
- #706984 lyricnz: Add FeedsSimplePie::parseExtensions() to allow parsing to be
customized.
- #728854 Scott Reynolds: Fix $queue->createItem() fails.
- #707098 alex_b: Improve performance of nodeapi and access checks.
- #726012 alex_b: Fix RSS descriptions not being reset in
common_syndication_parser.inc.
- alex_b: Fix a typo in the return value of process() in FeedsTermProcessor.
- alex_b: Stop PubSubHubbub from subscribing if it is not enabled.
- #711664 neclimdul: guarantee compatibility with CTools 1.4 by declaring that
Feeds uses hooks to define plugins via hook_ctools_plugin_plugins().
- #718460 jerdavis: In FeedsNodeProcessor, clear items only for the current
importer id.
- #718474 jerdavis: In FeedsNodeProcessor, check for duplicate items within
same importer id.
Feeds 6.x 1.0 Alpha 12, 2010-02-23
----------------------------------
- #600584 alex_b: PubSubHubbub support.
- alex_b: Debug log.
- alex_b: Add sourceSave() and sourceDelete() methods notifying plugin
implementers of a source being saved or deleted.
- #717168 nicholasThompson: Fix feeds UI JS doesn't select labels correctly.
- #708228 Scott Reynolds, alex_b: Break FeedsImportBatch into separate classes.
NOTE: Review your FeedsFetcher implementation for changes in the
FeedsImportBatch class, small adjustments may be necessary.
- alex_b: Support mapping to OpenID, using OpenID as a unique mapping target.
- alex_b: Handle exceptions outside of Importer/Source facade methods.
- #600584 alex_b: Use Batch API.
NOTE: third party plugins/extensions implementing FeedsProcessor::process(),
FeedsProcessor::clear() or FeedsImporter::expire() need to adjust their
implementations. Modules that directly use Feeds' API for importing or
clearing sources need may want to use feeds_batch_set() instead of
feeds_source()->import() or feeds_source()->clear().
Feeds 6.x 1.0 Alpha 11, 2010-02-10
----------------------------------
- #701432 pounard, Will White: Fix array_shift() expects parameter 1 is Array
error. Note: Parsers are responsible to ensure that the parameter passed to
FeedsImportBatch::setItems() is an Array.
- #698356 alex_b: Refactor and clean up FeedsScheduler::work() to allow more
scheduled tasks than 'import' and 'expire'.
Feeds 6.x 1.0 Alpha 10, 2010-01-25
----------------------------------
- #647128 bigkevmcd, Michelle: Fix broken author info in FeedsSyndicationParser.
- alex_b: Add mapping API for FeedsDataProcessor.
- alex_b: Decode HTML entities for title and author name in
FeedsSimplePieParser.
- #623448 David Goode, alex_b, et al.: Date mapper.
- #624088 mongolito404, David Goode, alex_b: Imagefield/filefield mapper,
formalize feed elements.
- #584034 aaroncouch, mongolito404: Views integration.
- Redirect to node or import form after manual import or delete.
- #663830 Aron Novak, alex_b: When download of URL failed, node w/ empty title
is created.
- #654728 Aron Novak: Fix parsing + data handling error with RDF 1.0 feeds.
- #641522 mongolito404, alex_b: Consolidate import stage results.
- #662104 Aron Novak: Specify PHP requirement in .info file.
- #657374 dtomasch: Common Parser does not get RSS Authors correctly.
Feeds 6.x 1.0 Alpha 9, 2009-12-14
---------------------------------
- API change: feeds_source() takes an FeedsImporter id instead of an importer,
the methods import() and clear() moved from FeedsImporter to FeedsSource.
Import from a source with feeds_source($id, $nid)->import();
- #629096 quickcel: Fix underscores in feed creation link.
- #652848 BWPanda: Add 'clear-block' to admin-ui to fix float issues.
- #623424 Kars-T, Eugen Mayer, alex_b: Mapper for Taxonomy.
- #649552 rsoden: Provide variable for data table name.
- #631962 velosol, alex_b: FeedsNodeProcessor: Update when changed.
- #623452 mongolito404: Port basic test infrastructure for mappers, test for
basic CCK mapper.
Feeds 6.x 1.0 Alpha 8, 2009-11-18
---------------------------------
- #634886 Kars-T, EugenMayer: Add vid to node process functions.
- #613494 miasma: Remove length limit from URL.
- #631050 z.stolar: Add feed_nid on node_load of a feed item.
- #631248 velosol: Set log message when creating a node in FeedsNodeProcessor.
Feeds 6.x 1.0 Alpha 7, 2009-11-04
---------------------------------
- #622654 Don't show body as option for mapper when body is disabled
- Allow cURL only to download via http or https
- Throw an exception in FeedsHTTPFetcher if result is not 200
Feeds 6.x 1.0 Alpha 6, 2009-11-03
---------------------------------
- Split number of items to queue on cron from feeds_schedule_num variable
(see README.txt)
- #619110 Fix node_delete() in FeedsNodeProcessor
- Add descriptions to all mapping sources and targets
Feeds 6.x 1.0 Alpha 5, 2009-10-23
---------------------------------
- #584500 Add Feeds default module
Feeds 6.x 1.0 Alpha 4, 2009-10-21
---------------------------------
- Initial release

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,204 @@
"It feeds"
FEEDS
=====
An import and aggregation framework for Drupal.
http://drupal.org/project/feeds
Features
========
- Pluggable import configurations consisting of fetchers (get data) parsers
(read and transform data) and processors (create content on Drupal).
-- HTTP upload (with optional PubSubHubbub support).
-- File upload.
-- CSV, RSS, Atom parsing.
-- Creates nodes or terms.
-- Creates lightweight database records if Data module is installed.
http://drupal.org/project/data
-- Additional fetchers/parsers or processors can be added by an object oriented
plugin system.
-- Granular mapping of parsed data to content elements.
- Import configurations can be piggy backed on nodes (thus using nodes to track
subscriptions to feeds) or they can be used on a standalone form.
- Unlimited number of import configurations.
- Export import configurations to code.
- Optional libraries module support.
Requirements
============
- CTools 1.x
http://drupal.org/project/ctools
- Job Scheduler
http://drupal.org/project/job_scheduler
- Drupal 7.x
http://drupal.org/project/drupal
- PHP safe mode is not supported, depending on your Feeds Importer configuration
safe mode may cause no problems though.
Installation
============
- Install Feeds, Feeds Admin UI.
- To get started quick, install one or all of the following Feature modules:
Feeds News, Feeds Import, Feeds Fast News (more info below).
- Make sure cron is correctly configured http://drupal.org/cron
- Go to import/ to import data.
- To use SimplePie parser, download either the compiled or minified SimplePie
and place simplepie_[version].compiled.php into feeds/libraries as
simplepie.compiled.php. Recommended version: 1.3.
http://simplepie.org/
Feature modules
===============
Feeds ships with three feature modules that can be enabled on
admin/build/modules or - if you are using Features - on admin/build/features.
http://drupal.org/project/features
The purpose of these modules is to provide a quick start for using Feeds. You
can either use them out of the box as they come or you can take them as samples
to learn how to build import or aggregation functionality with Feeds.
The feature modules merely contain sets of configurations using Feeds and in
some cases the modules Node, Views or Data. If the default configurations do not
fit your use case you can change them on the respective configuration pages for
Feeds, Node, Views or Data.
Here is a description of the provided feature modules:
- Feeds News -
This feature is a news aggregator. It provides a content type "Feed" that can
be used to subscribe to RSS or Atom feeds. Every item on such a feed is
aggregated as a node of the type "Feed item", also provided by the module.
What's neat about Feeds News is that it comes with a configured View that shows
a list of news items with every feed on the feed node's "View items" tab. It
also comes with an OPML importer filter that can be accessed under /import.
- Feeds Fast News -
This feature is very similar to Feeds News. The big difference is that instead
of aggregating a node for every item on a feed, it creates a database record
in a single table, thus significantly improving performance. This approach
especially starts to save resources when many items are being aggregated and
expired (= deleted) on a site.
- Feeds Import -
This feature is an example illustrating Feeds' import capabilities. It contains
a node importer and a user importer that can be accessed under /import. Both
accept CSV or TSV files as imports.
PubSubHubbub support
====================
Feeds supports the PubSubHubbub publish/subscribe protocol. Follow these steps
to set it up for your site.
http://code.google.com/p/pubsubhubbub/
- Go to admin/build/feeds and edit (override) the importer configuration you
would like to use for PubSubHubbub.
- Choose the HTTP Fetcher if it is not already selected.
- On the HTTP Fetcher, click on 'settings' and check "Use PubSubHubbub".
- Optionally you can use a designated hub such as http://superfeedr.com/ or your
own. If a designated hub is specified, every feed on this importer
configuration will be subscribed to this hub, no matter what the feed itself
specifies.
Libraries support
=================
If you are using Libraries module, you can place external libraries in the
Libraries module's search path (for instance sites/all/libraries. The only
external library used at the moment is SimplePie.
Libraries found in the libraries search path are preferred over libraries in
feeds/libraries/.
Transliteration support
=======================
If you plan to store files with Feeds - for instance when storing podcasts
or images from syndication feeds - it is recommended to enable the
Transliteration module to avoid issues with non-ASCII characters in file names.
http://drupal.org/project/transliteration
API Overview
============
See "The developer's guide to Feeds":
http://drupal.org/node/622700
Testing
=======
See "The developer's guide to Feeds":
http://drupal.org/node/622700
Debugging
=========
Set the Drupal variable 'feeds_debug' to TRUE (i. e. using drush). This will
create a file /tmp/feeds_[my_site_location].log. Use "tail -f" on the command
line to get a live view of debug output.
Note: at the moment, only PubSubHubbub related actions are logged.
Performance
===========
See "The site builder's guide to Feeds":
http://drupal.org/node/622698
Hidden settings
===============
Hidden settings are variables that you can define by adding them to the $conf
array in your settings.php file.
Name: feeds_debug
Default: FALSE
Description: Set to TRUE for enabling debug output to
/DRUPALTMPDIR/feeds_[sitename].log
Name: feeds_importer_class
Default: 'FeedsImporter'
Description: The class to use for importing feeds.
Name: feeds_source_class
Default: 'FeedsSource'
Description: The class to use for handling feed sources.
Name: feeds_data_$importer_id
Default: feeds_data_$importer_id
Description: The table used by FeedsDataProcessor to store feed items. Usually a
FeedsDataProcessor builds a table name from a prefix (feeds_data_)
and the importer's id ($importer_id). This default table name can
be overridden by defining a variable with the same name.
Name: feeds_process_limit
Default: 50
The number of nodes feed node processor creates or deletes in one
page load.
Name: http_request_timeout
Default: 15
Description: Timeout in seconds to wait for an HTTP get request to finish.
Name: feeds_never_use_curl
Default: FALSE
Description: Flag to stop feeds from using its cURL for http requests. See
http_request_use_curl().
Glossary
========
See "Feeds glossary":
http://drupal.org/node/622710

View File

@@ -0,0 +1,301 @@
<?php
/**
* @file
* Documentation of Feeds hooks.
*/
/**
* Feeds offers a CTools based plugin API. Fetchers, parsers and processors are
* declared to Feeds as plugins.
*
* @see feeds_feeds_plugins()
* @see FeedsFetcher
* @see FeedsParser
* @see FeedsProcessor
*
* @defgroup pluginapi Plugin API
* @{
*/
/**
* Example of a CTools plugin hook that needs to be implemented to make
* hook_feeds_plugins() discoverable by CTools and Feeds. The hook specifies
* that the hook_feeds_plugins() returns Feeds Plugin API version 1 style
* plugins.
*/
function hook_ctools_plugin_api($owner, $api) {
if ($owner == 'feeds' && $api == 'plugins') {
return array('version' => 1);
}
}
/**
* A hook_feeds_plugins() declares available Fetcher, Parser or Processor
* plugins to Feeds. For an example look at feeds_feeds_plugin(). For exposing
* this hook hook_ctools_plugin_api() MUST be implemented, too.
*
* @see feeds_feeds_plugin()
*/
function hook_feeds_plugins() {
$info = array();
$info['MyFetcher'] = array(
'name' => 'My Fetcher',
'description' => 'Fetches my stuff.',
'help' => 'More verbose description here. Will be displayed on fetcher selection menu.',
'handler' => array(
'parent' => 'FeedsFetcher',
'class' => 'MyFetcher',
'file' => 'MyFetcher.inc',
'path' => drupal_get_path('module', 'my_module'), // Feeds will look for MyFetcher.inc in the my_module directory.
),
);
$info['MyParser'] = array(
'name' => 'ODK parser',
'description' => 'Parse my stuff.',
'help' => 'More verbose description here. Will be displayed on parser selection menu.',
'handler' => array(
'parent' => 'FeedsParser', // Being directly or indirectly an extension of FeedsParser makes a plugin a parser plugin.
'class' => 'MyParser',
'file' => 'MyParser.inc',
'path' => drupal_get_path('module', 'my_module'),
),
);
$info['MyProcessor'] = array(
'name' => 'ODK parser',
'description' => 'Process my stuff.',
'help' => 'More verbose description here. Will be displayed on processor selection menu.',
'handler' => array(
'parent' => 'FeedsProcessor',
'class' => 'MyProcessor',
'file' => 'MyProcessor.inc',
'path' => drupal_get_path('module', 'my_module'),
),
);
return $info;
}
/**
* @}
*/
/**
* @defgroup import Import and clear hooks
* @{
*/
/**
* Invoked after a feed source has been parsed, before it will be processed.
*
* @param $source
* FeedsSource object that describes the source that has been imported.
* @param $result
* FeedsParserResult object that has been parsed from the source.
*/
function hook_feeds_after_parse(FeedsSource $source, FeedsParserResult $result) {
// For example, set title of imported content:
$result->title = 'Import number ' . my_module_import_id();
}
/**
* Invoked before a feed item is saved.
*
* @param $source
* FeedsSource object that describes the source that is being imported.
* @param $entity
* The entity object.
* @param $item
* The parser result for this entity.
*/
function hook_feeds_presave(FeedsSource $source, $entity, $item) {
if ($entity->feeds_item->entity_type == 'node') {
// Skip saving this entity.
$entity->feeds_item->skip = TRUE;
}
}
/**
* Invoked after a feed source has been imported.
*
* @param $source
* FeedsSource object that describes the source that has been imported.
*/
function hook_feeds_after_import(FeedsSource $source) {
// See geotaxonomy module's implementation for an example.
}
/**
* Invoked after a feed source has been cleared of its items.
*
* @param $source
* FeedsSource object that describes the source that has been cleared.
*/
function hook_feeds_after_clear(FeedsSource $source) {
}
/**
* @}
*/
/**
* @defgroup mappingapi Mapping API
* @{
*/
/**
* Alter mapping sources.
*
* Use this hook to add additional mapping sources for any parser. Allows for
* registering a callback to be invoked at mapping time.
*
* @see my_source_get_source().
* @see locale_feeds_parser_sources_alter().
*/
function hook_feeds_parser_sources_alter(&$sources, $content_type) {
$sources['my_source'] = array(
'name' => t('Images in description element'),
'description' => t('Images occuring in the description element of a feed item.'),
'callback' => 'my_source_get_source',
);
}
/**
* Example callback specified in hook_feeds_parser_sources_alter().
*
* To be invoked on mapping time.
*
* @param $source
* The FeedsSource object being imported.
* @param $result
* The FeedsParserResult object being mapped from.
* @param $key
* The key specified in the $sources array in
* hook_feeds_parser_sources_alter().
*
* @return
* The value to be extracted from the source.
*
* @see hook_feeds_parser_sources_alter().
* @see locale_feeds_get_source().
*/
function my_source_get_source($source, FeedsParserResult $result, $key) {
$item = $result->currentItem();
return my_source_parse_images($item['description']);
}
/**
* Alter mapping targets for entities. Use this hook to add additional target
* options to the mapping form of Node processors.
*
* If the key in $targets[] does not correspond to the actual key on the node
* object ($node->key), real_target MUST be specified. See mappers/link.inc
*
* For an example implementation, see mappers/content.inc
*
* @param &$targets
* Array containing the targets to be offered to the user. Add to this array
* to expose additional options. Remove from this array to suppress options.
* Remove with caution.
* @param $entity_type
* The entity type of the target, for instance a 'node' entity.
* @param $bundle_name
* The bundle name for which to alter targets.
*/
function hook_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
if ($entity_type == 'node') {
$targets['my_node_field'] = array(
'name' => t('My custom node field'),
'description' => t('Description of what my custom node field does.'),
'callback' => 'my_module_set_target',
// Specify both summary_callback and form_callback to add a per mapping
// configuration form.
'summary_callback' => 'my_module_summary_callback',
'form_callback' => 'my_module_form_callback',
);
$targets['my_node_field2'] = array(
'name' => t('My Second custom node field'),
'description' => t('Description of what my second custom node field does.'),
'callback' => 'my_module_set_target2',
'real_target' => 'my_node_field_two', // Specify real target field on node.
);
}
}
/**
* Example callback specified in hook_feeds_processor_targets_alter().
*
* @param $source
* Field mapper source settings.
* @param $entity
* An entity object, for instance a node object.
* @param $target
* A string identifying the target on the node.
* @param $value
* The value to populate the target with.
* @param $mapping
* Associative array of the mapping settings from the per mapping
* configuration form.
*/
function my_module_set_target($source, $entity, $target, $value, $mapping) {
$entity->{$target}[$entity->language][0]['value'] = $value;
if (isset($source->importer->processor->config['input_format'])) {
$entity->{$target}[$entity->language][0]['format'] =
$source->importer->processor->config['input_format'];
}
}
/**
* Example of the summary_callback specified in
* hook_feeds_processor_targets_alter().
*
* @param $mapping
* Associative array of the mapping settings.
* @param $target
* Array of target settings, as defined by the processor or
* hook_feeds_processor_targets_alter().
* @param $form
* The whole mapping form.
* @param $form_state
* The form state of the mapping form.
*
* @return
* Returns, as a string that may contain HTML, the summary to display while
* the full form isn't visible.
* If the return value is empty, no summary and no option to view the form
* will be displayed.
*/
function my_module_summary_callback($mapping, $target, $form, $form_state) {
if (empty($mapping['my_setting'])) {
return t('My setting <strong>not</strong> active');
}
else {
return t('My setting <strong>active</strong>');
}
}
/**
* Example of the form_callback specified in
* hook_feeds_processor_targets_alter().
*
* The arguments are the same that my_module_summary_callback() gets.
*
* @see my_module_summary_callback()
*
* @return
* The per mapping configuration form. Once the form is saved, $mapping will
* be populated with the form values.
*/
function my_module_form_callback($mapping, $target, $form, $form_state) {
return array(
'my_setting' => array(
'#type' => 'checkbox',
'#title' => t('My setting checkbox'),
'#default_value' => !empty($mapping['my_setting']),
),
);
}
/**
* @}
*/

View File

@@ -0,0 +1,7 @@
#edit-feeds-FeedsFileFetcher-upload-wrapper .file-info {
float: left;
width: 200px;
border-right: 1px solid #ddd;
margin-right: 10px;
}

View File

@@ -0,0 +1,43 @@
name = Feeds
description = Aggregates RSS/Atom/RDF feeds, imports CSV files and more.
package = Feeds
core = 7.x
dependencies[] = ctools
dependencies[] = job_scheduler
files[] = includes/FeedsConfigurable.inc
files[] = includes/FeedsImporter.inc
files[] = includes/FeedsSource.inc
files[] = libraries/ParserCSV.inc
files[] = libraries/http_request.inc
files[] = libraries/PuSHSubscriber.inc
files[] = tests/feeds.test
files[] = tests/feeds_date_time.test
files[] = tests/feeds_mapper_date.test
files[] = tests/feeds_mapper_field.test
files[] = tests/feeds_mapper_file.test
files[] = tests/feeds_mapper_path.test
files[] = tests/feeds_mapper_profile.test
files[] = tests/feeds_mapper.test
files[] = tests/feeds_mapper_config.test
files[] = tests/feeds_fetcher_file.test
files[] = tests/feeds_processor_node.test
files[] = tests/feeds_processor_term.test
files[] = tests/feeds_processor_user.test
files[] = tests/feeds_scheduler.test
files[] = tests/feeds_mapper_link.test
files[] = tests/feeds_mapper_taxonomy.test
files[] = tests/parser_csv.test
files[] = views/feeds_views_handler_argument_importer_id.inc
files[] = views/feeds_views_handler_field_importer_name.inc
files[] = views/feeds_views_handler_field_log_message.inc
files[] = views/feeds_views_handler_field_severity.inc
files[] = views/feeds_views_handler_field_source.inc
files[] = views/feeds_views_handler_filter_severity.inc
; Information added by drupal.org packaging script on 2012-10-24
version = "7.x-2.0-alpha7"
core = "7.x"
project = "feeds"
datestamp = "1351111319"

View File

@@ -0,0 +1,563 @@
<?php
/**
* @file
* Schema definitions install/update/uninstall hooks.
*/
/**
* Implements hook_schema().
*/
function feeds_schema() {
$schema = array();
$schema['feeds_importer'] = array(
'description' => 'Configuration of feeds objects.',
'export' => array(
'key' => 'id',
'identifier' => 'feeds_importer',
'default hook' => 'feeds_importer_default', // Function hook name.
'api' => array(
'owner' => 'feeds',
'api' => 'feeds_importer_default', // Base name for api include files.
'minimum_version' => 1,
'current_version' => 1,
),
),
'fields' => array(
'id' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Id of the fields object.',
),
'config' => array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'Configuration of the feeds object.',
'serialize' => TRUE,
),
),
'primary key' => array('id'),
);
$schema['feeds_source'] = array(
'description' => 'Source definitions for feeds.',
'fields' => array(
'id' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Id of the feed configuration.',
),
'feed_nid' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'unsigned' => TRUE,
'description' => 'Node nid if this particular source is attached to a feed node.',
),
'config' => array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'Configuration of the source.',
'serialize' => TRUE,
),
'source' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'Main source resource identifier. E. g. a path or a URL.',
),
'state' => array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'State of import or clearing batches.',
'serialize' => TRUE,
),
'fetcher_result' => array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'Cache for fetcher result.',
'serialize' => TRUE,
),
'imported' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'unsigned' => TRUE,
'description' => 'Timestamp when this source was imported last.',
),
),
'primary key' => array('id', 'feed_nid'),
'indexes' => array(
'id' => array('id'),
'feed_nid' => array('feed_nid'),
'id_source' => array('id', array('source', 128)),
),
);
$schema['feeds_item'] = array(
'description' => 'Tracks items such as nodes, terms, users.',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type.',
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The imported entity\'s serial id.',
),
'id' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The id of the importer that created this item.',
),
'feed_nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'Node id of the source, if available.',
),
'imported' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Import date of the feed item, as a Unix timestamp.',
),
'url' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'Link to the feed item.',
),
'guid' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'Unique identifier for the feed item.'
),
'hash' => array(
'type' => 'varchar',
'length' => 32, // The length of an MD5 hash.
'not null' => TRUE,
'default' => '',
'description' => 'The hash of the source item.',
),
),
'primary key' => array('entity_type', 'entity_id'),
'indexes' => array(
'id' => array('id'),
'feed_nid' => array('feed_nid'),
'lookup_url' => array('entity_type', 'id', 'feed_nid', array('url', 128)),
'lookup_guid' => array('entity_type', 'id', 'feed_nid', array('guid', 128)),
'global_lookup_url' => array('entity_type', array('url', 128)),
'global_lookup_guid' => array('entity_type', array('guid', 128)),
'imported' => array('imported'),
),
);
$schema['feeds_push_subscriptions'] = array(
'description' => 'PubSubHubbub subscriptions.',
'fields' => array(
'domain' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Domain of the subscriber. Corresponds to an importer id.',
),
'subscriber_id' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'unsigned' => TRUE,
'description' => 'ID of the subscriber. Corresponds to a feed nid.',
),
'timestamp' => array(
'type' => 'int',
'unsigned' => FALSE,
'default' => 0,
'not null' => TRUE,
'description' => 'Created timestamp.',
),
'hub' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'The URL of the hub endpoint of this subscription.',
),
'topic' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'The topic URL (feed URL) of this subscription.',
),
'secret' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Shared secret for message authentication.',
),
'status' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
'description' => 'Status of subscription.',
),
'post_fields' => array(
'type' => 'text',
'not null' => FALSE,
'description' => 'Fields posted.',
'serialize' => TRUE,
),
),
'primary key' => array('domain', 'subscriber_id'),
'indexes' => array(
'timestamp' => array('timestamp'),
),
);
$schema['feeds_log'] = array(
'description' => 'Table that contains logs of feeds events.',
'fields' => array(
'flid' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Primary Key: Unique feeds event ID.',
),
'id' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The id of the importer that logged the event.',
),
'feed_nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'Node id of the source, if available.',
),
'log_time' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Unix timestamp of when event occurred.',
),
'request_time' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Unix timestamp of the request when the event occurred.',
),
'type' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
'description' => 'Type of log message, for example "feeds_import"."',
),
'message' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'description' => 'Text of log message to be passed into the t() function.',
),
'variables' => array(
'type' => 'blob',
'not null' => TRUE,
'size' => 'big',
'description' => 'Serialized array of variables that match the message string and that is passed into the t() function.',
),
'severity' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'The severity level of the event; ranges from 0 (Emergency) to 7 (Debug)',
),
),
'primary key' => array('flid'),
'indexes' => array(
'id' => array('id'),
'id_feed_nid' => array('id', 'feed_nid'),
'request_time' => array('request_time'),
'log_time' => array('log_time'),
'type' => array('type'),
),
);
return $schema;
}
/**
* Rename feeds_source.batch to feeds_source.state, add slot for caching fetcher
* result.
*/
function feeds_update_7100() {
$spec = array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
'description' => 'State of import or clearing batches.',
'serialize' => TRUE,
);
db_change_field('feeds_source', 'batch', 'state', $spec);
$spec = array(
'type' => 'text',
'size' => 'big',
'not null' => FALSE,
'description' => 'Cache for fetcher result.',
'serialize' => TRUE,
);
db_add_field('feeds_source', 'fetcher_result', $spec);
}
/**
* Add imported timestamp to feeds_source table.
*/
function feeds_update_7201() {
$spec = array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'unsigned' => TRUE,
'description' => 'Timestamp when this source was imported last.',
);
db_add_field('feeds_source', 'imported', $spec);
}
/**
* Create a single feeds_item table tracking all imports.
*/
function feeds_update_7202() {
$spec = array(
'description' => 'Tracks items such as nodes, terms, users.',
'fields' => array(
'entity_type' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type.',
),
'entity_id' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The imported entity\'s serial id.',
),
'id' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The id of the importer that created this item.',
),
'feed_nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'Node id of the source, if available.',
),
'imported' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Import date of the feed item, as a Unix timestamp.',
),
'url' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'Link to the feed item.',
),
'guid' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'Unique identifier for the feed item.'
),
'hash' => array(
'type' => 'varchar',
'length' => 32, // The length of an MD5 hash.
'not null' => TRUE,
'default' => '',
'description' => 'The hash of the source item.',
),
),
'primary key' => array('entity_type', 'entity_id'),
'indexes' => array(
'id' => array('id'),
'feed_nid' => array('feed_nid'),
'lookup_url' => array('entity_type', 'id', 'feed_nid', array('url', 128)),
'lookup_guid' => array('entity_type', 'id', 'feed_nid', array('guid', 128)),
'imported' => array('imported'),
),
);
db_create_table('feeds_item', $spec);
// Copy all existing values from old tables and drop them.
$insert = "INSERT INTO {feeds_item} (entity_type, entity_id, id, feed_nid, imported, url, guid, hash)";
db_query($insert . " SELECT 'node', nid, id, feed_nid, imported, url, guid, hash FROM {feeds_node_item}");
db_query($insert . " SELECT 'taxonomy_term', tid, id, feed_nid, 0, '', '', '' FROM {feeds_term_item}");
db_drop_table('feeds_node_item');
db_drop_table('feeds_term_item');
}
/**
* Add feeds_log table.
*/
function feeds_update_7203() {
$schema = array(
'description' => 'Table that contains logs of feeds events.',
'fields' => array(
'flid' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Primary Key: Unique feeds event ID.',
),
'id' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The id of the importer that logged the event.',
),
'feed_nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'Node id of the source, if available.',
),
'log_time' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Unix timestamp of when event occurred.',
),
'request_time' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Unix timestamp of the request when the event occurred.',
),
'type' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
'description' => 'Type of log message, for example "feeds_import"."',
),
'message' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'description' => 'Text of log message to be passed into the t() function.',
),
'variables' => array(
'type' => 'blob',
'not null' => TRUE,
'size' => 'big',
'description' => 'Serialized array of variables that match the message string and that is passed into the t() function.',
),
'severity' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'The severity level of the event; ranges from 0 (Emergency) to 7 (Debug)',
),
),
'primary key' => array('flid'),
'indexes' => array(
'id' => array('id'),
'id_feed_nid' => array('id', 'feed_nid'),
'request_time' => array('request_time'),
'log_time' => array('log_time'),
'type' => array('type'),
),
);
db_create_table('feeds_log', $schema);
}
/**
* Add index for looking up by entity_type + url/ guid to feeds_item table.
*/
function feeds_update_7204() {
db_add_index('feeds_item', 'global_lookup_url', array('entity_type', array('url', 128)));
db_add_index('feeds_item', 'global_lookup_guid', array('entity_type', array('guid', 128)));
}
/**
* Shorten {feeds_item}.entity_type to 32 chars and shorten relevant indexes.
*/
function feeds_update_7205() {
db_drop_primary_key('feeds_item');
db_drop_index('feeds_item', 'lookup_url');
db_drop_index('feeds_item', 'lookup_guid');
db_drop_index('feeds_item', 'global_lookup_url');
db_drop_index('feeds_item', 'global_lookup_guid');
db_change_field('feeds_item', 'entity_type', 'entity_type', array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The entity type.',
));
db_add_primary_key('feeds_item', array('entity_type', 'entity_id'));
db_add_index('feeds_item', 'lookup_url', array('entity_type', 'id', 'feed_nid', array('url', 128)));
db_add_index('feeds_item', 'lookup_guid', array('entity_type', 'id', 'feed_nid', array('guid', 128)));
db_add_index('feeds_item', 'global_lookup_url', array('entity_type', array('url', 128)));
db_add_index('feeds_item', 'global_lookup_guid', array('entity_type', array('guid', 128)));
}
/**
* Change state and fetcher_result fields from text to blob.
*/
function feeds_update_7206() {
db_change_field('feeds_source', 'state', 'state', array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'State of import or clearing batches.',
'serialize' => TRUE,
));
db_change_field('feeds_source', 'fetcher_result', 'fetcher_result', array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'Cache for fetcher result.',
'serialize' => TRUE,
));
}
/**
* Change config fields from text to big blobs.
*/
function feeds_update_7207() {
db_change_field('feeds_importer', 'config', 'config', array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'Configuration of the feeds object.',
'serialize' => TRUE,
));
db_change_field('feeds_source', 'config', 'config', array(
'type' => 'blob',
'size' => 'big',
'not null' => FALSE,
'description' => 'Configuration of the feeds object.',
'serialize' => TRUE,
));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,362 @@
<?php
/**
* @file
* Menu callbacks, form callbacks and helpers.
*/
/**
* Render a page of available importers.
*/
function feeds_page() {
$rows = array();
if ($importers = feeds_importer_load_all()) {
foreach ($importers as $importer) {
if ($importer->disabled) {
continue;
}
if (!(user_access('import ' . $importer->id . ' feeds') || user_access('administer feeds'))) {
continue;
}
if (empty($importer->config['content_type'])) {
$link = 'import/' . $importer->id;
$title = $importer->config['name'];
}
elseif (node_access('create', $importer->config['content_type'])) {
$link = 'node/add/' . str_replace('_', '-', $importer->config['content_type']);
$title = node_type_get_name($importer->config['content_type']);
}
else {
continue;
}
$rows[] = array(
l($title, $link),
check_plain($importer->config['description']),
);
}
}
if (empty($rows)) {
drupal_set_message(t('There are no importers, go to <a href="@importers">Feed importers</a> to create one or enable an existing one.', array('@importers' => url('admin/structure/feeds'))));
}
$header = array(
t('Import'),
t('Description'),
);
return theme('table', array('header' => $header, 'rows' => $rows));
}
/**
* Render a feeds import form on import/[config] pages.
*/
function feeds_import_form($form, &$form_state, $importer_id) {
$source = feeds_source($importer_id, empty($form['nid']['#value']) ? 0 : $form['nid']['#value']);
$form = array();
$form['#importer_id'] = $importer_id;
// @todo Move this into fetcher?
$form['#attributes']['enctype'] = 'multipart/form-data';
$form['source_status'] = array(
'#type' => 'fieldset',
'#title' => t('Status'),
'#tree' => TRUE,
'#value' => feeds_source_status($source),
);
$source_form = $source->configForm($form_state);
if (!empty($source_form)) {
$form['feeds'] = array(
'#type' => 'fieldset',
'#title' => t('Import'),
'#tree' => TRUE,
) + $source_form;
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Import'),
);
$progress = $source->progressImporting();
if ($progress !== FEEDS_BATCH_COMPLETE) {
$form['submit']['#disabled'] = TRUE;
$form['submit']['#value'] =
t('Importing (@progress %)', array('@progress' => number_format(100 * $progress, 0)));
}
return $form;
}
/**
* Validation handler for node forms and feeds_import_form().
*/
function feeds_import_form_validate($form, &$form_state) {
// @todo This may be a problem here, as we don't have a feed_nid at this point.
feeds_source($form['#importer_id'])->configFormValidate($form_state['values']['feeds']);
}
/**
* Submit handler for feeds_import_form().
*/
function feeds_import_form_submit($form, &$form_state) {
// Save source and import.
$source = feeds_source($form['#importer_id']);
if (!empty($form_state['values']['feeds']) && is_array($form_state['values']['feeds'])) {
$source->addConfig($form_state['values']['feeds']);
$source->save();
}
// Refresh feed if import on create is selected.
if ($source->importer->config['import_on_create']) {
$source->startImport();
}
// Add to schedule, make sure importer is scheduled, too.
$source->schedule();
$source->importer->schedule();
}
/**
* Render a feeds import form on node/id/import pages.
*/
function feeds_import_tab_form($form, &$form_state, $node) {
$importer_id = feeds_get_importer_id($node->type);
$source = feeds_source($importer_id, $node->nid);
$form = array();
$form['#feed_nid'] = $node->nid;
$form['#importer_id'] = $importer_id;
$form['#redirect'] = 'node/' . $node->nid;
$form['source_status'] = array(
'#type' => 'fieldset',
'#title' => t('Status'),
'#tree' => TRUE,
'#value' => feeds_source_status($source),
);
$form = confirm_form($form, t('Import all content from source?'), 'node/' . $node->nid, '', t('Import'), t('Cancel'), 'confirm feeds update');
$progress = $source->progressImporting();
if ($progress !== FEEDS_BATCH_COMPLETE) {
$form['actions']['submit']['#disabled'] = TRUE;
$form['actions']['submit']['#value'] =
t('Importing (@progress %)', array('@progress' => number_format(100 * $progress, 0)));
}
return $form;
}
/**
* Submit handler for feeds_import_tab_form().
*/
function feeds_import_tab_form_submit($form, &$form_state) {
$form_state['redirect'] = $form['#redirect'];
feeds_source($form['#importer_id'], $form['#feed_nid'])->startImport();
}
/**
* Render a feeds delete form.
*
* Used on both node pages and configuration pages.
* Therefore $node may be missing.
*/
function feeds_delete_tab_form($form, &$form_state, $importer_id, $node = NULL) {
if (empty($node)) {
$source = feeds_source($importer_id);
$form['#redirect'] = 'import/' . $source->id;
}
else {
$importer_id = feeds_get_importer_id($node->type);
$source = feeds_source($importer_id, $node->nid);
$form['#redirect'] = 'node/' . $source->feed_nid;
}
// Form cannot pass on source object.
$form['#importer_id'] = $source->id;
$form['#feed_nid'] = $source->feed_nid;
$form['source_status'] = array(
'#type' => 'fieldset',
'#title' => t('Status'),
'#tree' => TRUE,
'#value' => feeds_source_status($source),
);
$form = confirm_form($form, t('Delete all items from source?'), $form['#redirect'], '', t('Delete'), t('Cancel'), 'confirm feeds update');
$progress = $source->progressClearing();
if ($progress !== FEEDS_BATCH_COMPLETE) {
$form['actions']['submit']['#disabled'] = TRUE;
$form['actions']['submit']['#value'] =
t('Deleting (@progress %)', array('@progress' => number_format(100 * $progress, 0)));
}
return $form;
}
/**
* Submit handler for feeds_delete_tab_form().
*/
function feeds_delete_tab_form_submit($form, &$form_state) {
$form_state['redirect'] = $form['#redirect'];
$feed_nid = empty($form['#feed_nid']) ? 0 : $form['#feed_nid'];
feeds_source($form['#importer_id'], $feed_nid)->startClear();
}
/**
* Render a feeds unlock form.
*
* Used on both node pages and configuration pages.
* Therefore $node may be missing.
*/
function feeds_unlock_tab_form($form, &$form_state, $importer_id, $node = NULL) {
if (empty($node)) {
$source = feeds_source($importer_id);
$form['#redirect'] = 'import/' . $source->id;
}
else {
$importer_id = feeds_get_importer_id($node->type);
$source = feeds_source($importer_id, $node->nid);
$form['#redirect'] = 'node/' . $source->feed_nid;
}
// Form cannot pass on source object.
$form['#importer_id'] = $source->id;
$form['#feed_nid'] = $source->feed_nid;
$form['source_status'] = array(
'#type' => 'fieldset',
'#title' => t('Status'),
'#tree' => TRUE,
'#value' => feeds_source_status($source),
);
$form = confirm_form($form, t('Unlock this importer?'), $form['#redirect'], '', t('Delete'), t('Cancel'), 'confirm feeds update');
if ($source->progressImporting() == FEEDS_BATCH_COMPLETE && $source->progressClearing() == FEEDS_BATCH_COMPLETE) {
$form['source_locked'] = array(
'#type' => 'markup',
'#title' => t('Not Locked'),
'#tree' => TRUE,
'#markup' => t('This importer is not locked, therefore it cannot be unlocked.'),
);
$form['actions']['submit']['#disabled'] = TRUE;
$form['actions']['submit']['#value'] = t('Unlock (disabled)');
}
else {
$form['actions']['submit']['#value'] = t('Unlock');
}
return $form;
}
/**
* Form submit handler. Resets all feeds state.
*/
function feeds_unlock_tab_form_submit($form, &$form_state) {
drupal_set_message(t('Import Unlocked'));
$form_state['redirect'] = $form['#redirect'];
$feed_nid = empty($form['#feed_nid']) ? 0 : $form['#feed_nid'];
$importer_id = $form['#importer_id'];
//Is there a more API-friendly way to set the state?
db_update('feeds_source')
->condition('id', $importer_id)
->condition('feed_nid', $feed_nid)
->fields(array('state' => FALSE))
->execute();
}
/**
* Handle a fetcher callback.
*/
function feeds_fetcher_callback($importer, $feed_nid = 0) {
if ($importer instanceof FeedsImporter) {
try {
return $importer->fetcher->request($feed_nid);
}
catch (Exception $e) {
// Do nothing.
}
}
drupal_access_denied();
}
/**
* Template generation
*/
function feeds_importer_template($importer_id) {
$importer = feeds_importer($importer_id);
if ($importer->parser instanceof FeedsCSVParser) {
return $importer->parser->getTemplate();
}
return drupal_not_found();
}
/**
* Renders a status display for a source.
*/
function feeds_source_status($source) {
$progress_importing = $source->progressImporting();
$v = array();
if ($progress_importing != FEEDS_BATCH_COMPLETE) {
$v['progress_importing'] = $progress_importing;
}
$progress_clearing = $source->progressClearing();
if ($progress_clearing != FEEDS_BATCH_COMPLETE) {
$v['progress_clearing'] = $progress_clearing;
}
$v['imported'] = $source->imported;
$v['count'] = $source->itemCount();
if (!empty($v)) {
return theme('feeds_source_status', $v);
}
}
/**
* Themes a status display for a source.
*/
function theme_feeds_source_status($v) {
$output = '<div class="info-box feeds-source-status">';
$items = array();
if ($v['progress_importing']) {
$progress = number_format(100.0 * $v['progress_importing'], 0);
$items[] = t('Importing - @progress % complete.', array('@progress' => $progress));
}
if ($v['progress_clearing']) {
$progress = number_format(100.0 * $v['progress_clearing'], 0);
$items[] = t('Deleting items - @progress % complete.', array('@progress' => $progress));
}
if (!count($items)) {
if ($v['count']) {
if ($v['imported']) {
$items[] = t('Last import: @ago ago.', array('@ago' => format_interval(REQUEST_TIME - $v['imported'], 1)));
}
$items[] = t('@count imported items total.', array('@count' => $v['count']));
}
else {
$items[] = t('No imported items.');
}
}
$output .= theme('item_list', array('items' => $items));
$output .= '</div>';
return $output;
}
/**
* Theme upload widget.
*/
function theme_feeds_upload($variables) {
$element = $variables['element'];
drupal_add_css(drupal_get_path('module', 'feeds') . '/feeds.css');
_form_set_class($element, array('form-file'));
$description = '';
if (!empty($element['#file_info'])) {
$file = $element['#file_info'];
$wrapper = file_stream_wrapper_get_instance_by_uri($file->uri);
$description .= '<div class="file-info">';
$description .= '<div class="file-name">';
$description .= l($file->filename, $wrapper->getExternalUrl());
$description .= '</div>';
$description .= '<div class="file-size">';
$description .= format_size($file->filesize);
$description .= '</div>';
$description .= '<div class="file-mime">';
$description .= check_plain($file->filemime);
$description .= '</div>';
$description .= '</div>';
}
$description .= '<div class="file-upload">';
$description .= '<input type="file" name="' . $element['#name'] . '"' . ($element['#attributes'] ? ' ' . drupal_attributes($element['#attributes']) : '') . ' id="' . $element['#id'] . '" size="' . $element['#size'] . "\" />\n";
$description .= '</div>';
$element['#description'] = $description;
// For some reason not unsetting #title leads to printing the title twice.
unset($element['#title']);
return theme('form_element', $element);
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* @file
* CTools plugins declarations.
*/
/**
* Break out for feeds_feed_plugins().
*/
function _feeds_feeds_plugins() {
$path = drupal_get_path('module', 'feeds') . '/plugins';
$info = array();
$info['FeedsPlugin'] = array(
'hidden' => TRUE,
'handler' => array(
'class' => 'FeedsPlugin',
'file' => 'FeedsPlugin.inc',
'path' => $path,
),
);
$info['FeedsMissingPlugin'] = array(
'hidden' => TRUE,
'handler' => array(
'class' => 'FeedsMissingPlugin',
'file' => 'FeedsPlugin.inc',
'path' => $path,
),
);
$info['FeedsFetcher'] = array(
'hidden' => TRUE,
'handler' => array(
'parent' => 'FeedsPlugin',
'class' => 'FeedsFetcher',
'file' => 'FeedsFetcher.inc',
'path' => $path,
),
);
$info['FeedsParser'] = array(
'hidden' => TRUE,
'handler' => array(
'parent' => 'FeedsPlugin',
'class' => 'FeedsParser',
'file' => 'FeedsParser.inc',
'path' => $path,
),
);
$info['FeedsProcessor'] = array(
'hidden' => TRUE,
'handler' => array(
'parent' => 'FeedsPlugin',
'class' => 'FeedsProcessor',
'file' => 'FeedsProcessor.inc',
'path' => $path,
),
);
$info['FeedsHTTPFetcher'] = array(
'name' => 'HTTP Fetcher',
'description' => 'Download content from a URL.',
'handler' => array(
'parent' => 'FeedsFetcher', // This is the key name, not the class name.
'class' => 'FeedsHTTPFetcher',
'file' => 'FeedsHTTPFetcher.inc',
'path' => $path,
),
);
$info['FeedsFileFetcher'] = array(
'name' => 'File upload',
'description' => 'Upload content from a local file.',
'handler' => array(
'parent' => 'FeedsFetcher',
'class' => 'FeedsFileFetcher',
'file' => 'FeedsFileFetcher.inc',
'path' => $path,
),
);
$info['FeedsCSVParser'] = array(
'name' => 'CSV parser',
'description' => 'Parse data in Comma Separated Value format.',
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'FeedsCSVParser',
'file' => 'FeedsCSVParser.inc',
'path' => $path,
),
);
$info['FeedsSyndicationParser'] = array(
'name' => 'Common syndication parser',
'description' => 'Parse RSS and Atom feeds.',
'help' => 'Parse XML feeds in RSS 1, RSS 2 and Atom format.',
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'FeedsSyndicationParser',
'file' => 'FeedsSyndicationParser.inc',
'path' => $path,
),
);
$info['FeedsOPMLParser'] = array(
'name' => 'OPML parser',
'description' => 'Parse OPML files.',
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'FeedsOPMLParser',
'file' => 'FeedsOPMLParser.inc',
'path' => $path,
),
);
if (feeds_simplepie_exists()) {
$info['FeedsSimplePieParser'] = array(
'name' => 'SimplePie parser',
'description' => 'Parse RSS and Atom feeds.',
'help' => 'Use <a href="http://simplepie.org">SimplePie</a> to parse XML feeds in RSS 1, RSS 2 and Atom format.',
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'FeedsSimplePieParser',
'file' => 'FeedsSimplePieParser.inc',
'path' => $path,
),
);
}
$info['FeedsSitemapParser'] = array(
'name' => 'Sitemap parser',
'description' => 'Parse Sitemap XML format feeds.',
'handler' => array(
'parent' => 'FeedsParser',
'class' => 'FeedsSitemapParser',
'file' => 'FeedsSitemapParser.inc',
'path' => $path,
),
);
$info['FeedsNodeProcessor'] = array(
'name' => 'Node processor',
'description' => 'Create and update nodes.',
'help' => 'Create and update nodes from parsed content.',
'handler' => array(
'parent' => 'FeedsProcessor',
'class' => 'FeedsNodeProcessor',
'file' => 'FeedsNodeProcessor.inc',
'path' => $path,
),
);
$info['FeedsUserProcessor'] = array(
'name' => 'User processor',
'description' => 'Create users.',
'help' => 'Create users from parsed content.',
'handler' => array(
'parent' => 'FeedsProcessor',
'class' => 'FeedsUserProcessor',
'file' => 'FeedsUserProcessor.inc',
'path' => $path,
),
);
if (module_exists('taxonomy')) {
$info['FeedsTermProcessor'] = array(
'name' => 'Taxonomy term processor',
'description' => 'Create taxonomy terms.',
'help' => 'Create taxonomy terms from parsed content.',
'handler' => array(
'parent' => 'FeedsProcessor',
'class' => 'FeedsTermProcessor',
'file' => 'FeedsTermProcessor.inc',
'path' => $path,
),
);
}
return $info;
}

View File

@@ -0,0 +1,89 @@
<?php
/**
* @file
* Rules integration.
*/
/**
* Implements hook_rules_event_info().
*/
function feeds_rules_event_info() {
$info = array();
$entity_info = entity_get_info();
foreach (feeds_importer_load_all() as $importer) {
$config = $importer->getConfig();
$processor = feeds_plugin($config['processor']['plugin_key'], $importer->id);
// It's possible to get FeedsMissingPlugin here which will break things
// since it doesn't implement FeedsProcessor::entityType().
if (!$processor instanceof FeedsProcessor) {
continue;
}
$entity_type = $processor->entityType();
$label = isset($entity_info[$entity_type]['label']) ? $entity_info[$entity_type]['label'] : $entity_type;
$info['feeds_import_'. $importer->id] = array(
'label' => t('Before saving an item imported via @name.', array('@name' => $importer->config['name'])),
'group' => t('Feeds'),
'variables' => array(
$entity_type => array(
'label' => t('Imported @label', array('@label' => $label)),
'type' => $entity_type,
// Saving is handled by feeds anyway (unless the skip action is used).
'skip save' => TRUE,
),
),
'access callback' => 'feeds_rules_access_callback',
);
// Add bundle information if the node processor is used.
if ($processor instanceof FeedsNodeProcessor) {
$config = $processor->getConfig();
$info['feeds_import_'. $importer->id]['variables'][$entity_type]['bundle'] = $config['content_type'];
}
}
return $info;
}
/**
* Implements of hook_rules_action_info().
*/
function feeds_rules_action_info() {
return array(
'feeds_skip_item' => array(
'base' => 'feeds_action_skip_item',
'label' => t('Skip import of feeds item'),
'group' => t('Feeds'),
'parameter' => array(
'entity' => array('type' => 'entity', 'label' => t('The feeds import item to be marked as skipped')),
),
'access callback' => 'feeds_rules_access_callback',
),
);
}
/**
* Mark feeds import item as skipped.
*/
function feeds_action_skip_item($entity_wrapper) {
$entity = $entity_wrapper->value();
if (isset($entity->feeds_item)) {
$entity->feeds_item->skip = TRUE;
}
}
/**
* Help callback for the skip action.
*/
function feeds_action_skip_item_help() {
return t("This action allows skipping certain feed items during feeds processing, i.e. before an imported item is saved. Once this action is used on a item, the changes to the entity of the feed item are not saved.");
}
/**
* Access callback for the feeds rules integration.
*/
function feeds_rules_access_callback() {
return user_access('administer feeds');
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* @file
* Builds placeholder replacement tokens for feed-related data.
*/
/**
* Implements hook_token_info().
*/
function feeds_token_info() {
// @todo This token could be for any entity type.
$info['tokens']['node']['feed-source'] = array(
'name' => t('Feed source'),
'description' => t('The node the feed item was sourced from.'),
'type' => 'node',
);
return $info;
}
/**
* Implements hook_tokens().
*/
function feeds_tokens($type, $tokens, array $data, array $options) {
$replacements = array();
if ($type == 'node' && !empty($data['node'])) {
$sanitize = !empty($options['sanitize']);
$feed_nid = feeds_get_feed_nid($data['node']->nid, 'node');
if ($feed_nid && $feed_source = node_load($feed_nid)) {
foreach ($tokens as $name => $original) {
switch ($name) {
case 'feed-source':
$replacements[$original] = $sanitize ? check_plain($feed_source->title) : $feed_source->title;
break;
}
}
// Chained node token relationships.
if ($feed_source_tokens = token_find_with_prefix($tokens, 'feed-source')) {
$replacements += token_generate('node', $feed_source_tokens, array('node' => $feed_source), $options);
}
}
}
return $replacements;
}

View File

@@ -0,0 +1,15 @@
<?php
/**
* @file
* feeds_import.features.inc
*/
/**
* Implements hook_ctools_plugin_api().
*/
function feeds_import_ctools_plugin_api() {
list($module, $api) = func_get_args();
if ($module == "feeds" && $api == "feeds_importer_default") {
return array("version" => "1");
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* @file
* feeds_import.feeds_importer_default.inc
*/
/**
* Implements hook_feeds_importer_default().
*/
function feeds_import_feeds_importer_default() {
$export = array();
$feeds_importer = new stdClass();
$feeds_importer->disabled = FALSE; /* Edit this to true to make a default feeds_importer disabled initially */
$feeds_importer->api_version = 1;
$feeds_importer->id = 'node';
$feeds_importer->config = array(
'name' => 'Node import',
'description' => 'Import nodes from CSV file.',
'fetcher' => array(
'plugin_key' => 'FeedsFileFetcher',
'config' => array(
'direct' => FALSE,
),
),
'parser' => array(
'plugin_key' => 'FeedsCSVParser',
'config' => array(
'delimiter' => ',',
),
),
'processor' => array(
'plugin_key' => 'FeedsNodeProcessor',
'config' => array(
'content_type' => 'article',
'update_existing' => 1,
'expire' => '-1',
'mappings' => array(
0 => array(
'source' => 'title',
'target' => 'title',
'unique' => FALSE,
),
1 => array(
'source' => 'body',
'target' => 'body',
'unique' => FALSE,
),
2 => array(
'source' => 'published',
'target' => 'created',
'unique' => FALSE,
),
3 => array(
'source' => 'guid',
'target' => 'guid',
'unique' => 1,
),
),
'input_format' => 'plain_text',
'author' => 0,
),
),
'content_type' => '',
'update' => 0,
'import_period' => '-1',
'expire_period' => 3600,
'import_on_create' => 1,
);
$export['node'] = $feeds_importer;
$feeds_importer = new stdClass();
$feeds_importer->disabled = FALSE; /* Edit this to true to make a default feeds_importer disabled initially */
$feeds_importer->api_version = 1;
$feeds_importer->id = 'user';
$feeds_importer->config = array(
'name' => 'User import',
'description' => 'Import users from CSV file.',
'fetcher' => array(
'plugin_key' => 'FeedsFileFetcher',
'config' => array(
'direct' => FALSE,
),
),
'parser' => array(
'plugin_key' => 'FeedsCSVParser',
'config' => array(
'delimiter' => ',',
),
),
'processor' => array(
'plugin_key' => 'FeedsUserProcessor',
'config' => array(
'roles' => array(),
'update_existing' => FALSE,
'status' => 1,
'mappings' => array(
0 => array(
'source' => 'name',
'target' => 'name',
'unique' => 0,
),
1 => array(
'source' => 'mail',
'target' => 'mail',
'unique' => 1,
),
2 => array(
'source' => 'created',
'target' => 'created',
'unique' => FALSE,
),
),
),
),
'content_type' => '',
'update' => 0,
'import_period' => '-1',
'expire_period' => 3600,
'import_on_create' => 1,
);
$export['user'] = $feeds_importer;
return $export;
}

View File

@@ -0,0 +1,14 @@
name = Feeds Import
description = An example of a node importer and a user importer.
core = 7.x
package = Feeds
php = 5.2.4
version = 7.x-2.0-alpha7
project = feeds
dependencies[] = feeds
datestamp = 1351111319
features[ctools][] = feeds:feeds_importer_default:1
features[features_api][] = api:1
features[feeds_importer][] = node
features[feeds_importer][] = user
files[] = feeds_import.test

View File

@@ -0,0 +1,8 @@
<?php
/**
* @file
* Empty module file.
*/
include_once('feeds_import.features.inc');

View File

@@ -0,0 +1,145 @@
<?php
/**
* @file
* Tests for feeds_import feature.
*/
/**
* Test Node import configuration.
*/
class FeedsExamplesNodeTestCase extends FeedsWebTestCase {
/**
* Set up test.
*/
public function setUp() {
parent::setUp(array('feeds_import'));
}
public static function getInfo() {
return array(
'name' => 'Feature: Node import',
'description' => 'Test "Node import" default configuration.',
'group' => 'Feeds',
);
}
/**
* Run tests.
*/
public function test() {
// Import file.
$this->importFile('node', $this->absolutePath() . '/tests/feeds/nodes.csv');
// Assert returning page.
$this->assertText('Created 8 nodes');
$this->assertText('Import CSV files with one or more of these columns: title, body, published, guid.');
$this->assertText('Column guid is mandatory and considered unique: only one item per guid value will be created.');
$this->assertRaw('feeds/nodes.csv');
// Assert created nodes.
$this->drupalGet('node');
$this->assertText('Typi non habent');
$this->assertText('Eodem modo typi');
$this->assertText('Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.');
$this->assertText('Lorem ipsum');
$this->assertText('Ut wisi enim ad minim veniam');
$this->assertText('1976');
// Nam liber tempor has the same GUID as Lorem ipsum.
$this->assertNoText('Nam liber tempor');
// Click through to one node.
$this->clickLink('Lorem ipsum');
$this->assertText('Lorem ipsum');
$this->assertText('Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.');
$this->assertText('Anonymous');
// Assert DB status as is and again after an additional import.
for ($i = 0; $i < 2; $i++) {
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node'")->fetchField();
$this->assertEqual($count, 8, 'Found correct number of items.');
$count = db_query("SELECT COUNT(*) FROM {node} WHERE type = 'article' AND status = 1 AND uid = 0")->fetchField();
$this->assertEqual($count, 8, 'Found correct number of items.');
// Do not filter on type intentionally. There shouldn't be more than 8 nodes total.
$count = db_query("SELECT COUNT(*) FROM {node_revision}")->fetchField();
$this->assertEqual($count, 8, 'Found correct number of items.');
// Import again. Feeds only updates items that haven't changed. However,
// there are 2 different items with the same GUID in nodes.csv.
// Therefore, feeds will show updates to 2 nodes.
$this->drupalPost('import/node/import', array(), 'Import');
$this->assertText('Updated 2 nodes');
}
// Remove all nodes.
$this->drupalPost('import/node/delete-items', array(), 'Delete');
$this->assertText('Deleted 8 nodes');
// Import once again.
$this->drupalPost('import/node/import', array(), 'Import');
$this->assertText('Created 8 nodes');
// Import a similar file with changes in 4 records. Feeds should report 6
// Updated Article nodes (4 changed records, 2 records sharing a GUID
// subsequently being updated).
$this->importFile('node', $this->absolutePath() . '/tests/feeds/nodes_changes.csv');
$this->assertText('Updated 6 nodes');
// Import a larger file with more records.
$this->importFile('node', $this->absolutePath() . '/tests/feeds/many_nodes.csv');
$this->assertText('Created 71 nodes');
// Remove all nodes.
$this->drupalPost('import/node/delete-items', array(), 'Delete');
$this->assertText('Deleted 79 nodes');
// Import once again.
$this->drupalPost('import/node/import', array(), 'Import');
$this->assertText('Created 79 nodes');
$this->assertText('Updated 7 nodes');
// Import a tab separated file.
$this->drupalPost('import/node/delete-items', array(), 'Delete');
$edit = array(
'files[feeds]' => $this->absolutePath() . '/tests/feeds/nodes.tsv',
'feeds[FeedsCSVParser][delimiter]' => "TAB",
);
$this->drupalPost('import/node', $edit, 'Import');
$this->assertText('Created 8 nodes');
}
}
/**
* Test User import configuration.
*/
class FeedsExamplesUserTestCase extends FeedsWebTestCase {
public static function getInfo() {
return array(
'name' => 'Feature: User import',
'description' => 'Test "User import" default configuration.',
'group' => 'Feeds',
);
}
public function setUp() {
parent::setUp(array('feeds_import'));
}
/**
* Run tests.
*/
public function test() {
// Import CSV file.
$this->importFile('user', $this->absolutePath() . '/tests/feeds/users.csv');
// Assert result.
$this->assertText('Created 3 users');
// 1 user has an invalid email address.
$this->assertText('Failed importing 2 users');
$this->drupalGet('admin/people');
$this->assertText('Morticia');
$this->assertText('Fester');
$this->assertText('Gomez');
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* @file
* feeds_news.features.field.inc
*/
/**
* Implementation of hook_field_default_fields().
*/
function feeds_news_field_default_fields() {
$fields = array();
// Exported field: 'node-feed_item-field_feed_item_description'
$fields['node-feed_item-field_feed_item_description'] = array(
'field_config' => array(
'active' => '1',
'cardinality' => '1',
'deleted' => '0',
'entity_types' => array(),
'field_name' => 'field_feed_item_description',
'foreign keys' => array(
'format' => array(
'columns' => array(
'format' => 'format',
),
'table' => 'filter_format',
),
),
'indexes' => array(
'format' => array(
0 => 'format',
),
),
'module' => 'text',
'settings' => array(),
'translatable' => '1',
'type' => 'text_with_summary',
),
'field_instance' => array(
'bundle' => 'feed_item',
'default_value' => NULL,
'deleted' => '0',
'description' => '',
'display' => array(
'default' => array(
'label' => 'hidden',
'module' => 'text',
'settings' => array(),
'type' => 'text_default',
'weight' => '0',
),
'full' => array(
'label' => 'above',
'settings' => array(),
'type' => 'hidden',
'weight' => 0,
),
'rss' => array(
'label' => 'above',
'settings' => array(),
'type' => 'hidden',
'weight' => 0,
),
'teaser' => array(
'label' => 'hidden',
'module' => 'text',
'settings' => array(
'trim_length' => 600,
),
'type' => 'text_trimmed',
'weight' => '0',
),
),
'entity_type' => 'node',
'field_name' => 'field_feed_item_description',
'label' => 'Description',
'required' => 0,
'settings' => array(
'display_summary' => 0,
'text_processing' => '1',
'user_register_form' => FALSE,
),
'widget' => array(
'active' => 1,
'module' => 'text',
'settings' => array(
'rows' => '20',
'summary_rows' => 5,
),
'type' => 'text_textarea_with_summary',
'weight' => '-4',
),
),
);
// Translatables
// Included for use with string extractors like potx.
t('Description');
return $fields;
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* @file
* feeds_news.features.inc
*/
/**
* Implementation of hook_ctools_plugin_api().
*/
function feeds_news_ctools_plugin_api() {
list($module, $api) = func_get_args();
if ($module == "feeds" && $api == "feeds_importer_default") {
return array("version" => 1);
}
}
/**
* Implementation of hook_views_api().
*/
function feeds_news_views_api() {
list($module, $api) = func_get_args();
if ($module == "views" && $api == "views_default") {
return array("version" => 3.0);
}
}
/**
* Implementation of hook_node_info().
*/
function feeds_news_node_info() {
$items = array(
'feed' => array(
'name' => t('Feed'),
'base' => 'node_content',
'description' => t('Subscribe to RSS or Atom feeds. Creates nodes of the content type "Feed item" from feed content.'),
'has_title' => '1',
'title_label' => t('Title'),
'help' => '',
),
'feed_item' => array(
'name' => t('Feed item'),
'base' => 'node_content',
'description' => t('This content type is being used for automatically aggregated content from feeds.'),
'has_title' => '1',
'title_label' => t('Title'),
'help' => '',
),
);
return $items;
}

View File

@@ -0,0 +1,124 @@
<?php
/**
* @file
* feeds_news.feeds_importer_default.inc
*/
/**
* Implementation of hook_feeds_importer_default().
*/
function feeds_news_feeds_importer_default() {
$export = array();
$feeds_importer = new stdClass;
$feeds_importer->disabled = FALSE; /* Edit this to true to make a default feeds_importer disabled initially */
$feeds_importer->api_version = 1;
$feeds_importer->id = 'feed';
$feeds_importer->config = array(
'name' => 'Feed',
'description' => 'Import RSS or Atom feeds, create nodes from feed items.',
'fetcher' => array(
'plugin_key' => 'FeedsHTTPFetcher',
'config' => array(
'auto_detect_feeds' => 1,
'use_pubsubhubbub' => 0,
'designated_hub' => '',
),
),
'parser' => array(
'plugin_key' => 'FeedsSyndicationParser',
'config' => array(),
),
'processor' => array(
'plugin_key' => 'FeedsNodeProcessor',
'config' => array(
'content_type' => 'feed_item',
'update_existing' => '0',
'expire' => '-1',
'mappings' => array(
0 => array(
'source' => 'title',
'target' => 'title',
'unique' => FALSE,
),
1 => array(
'source' => 'timestamp',
'target' => 'created',
'unique' => FALSE,
),
2 => array(
'source' => 'url',
'target' => 'url',
'unique' => 1,
),
3 => array(
'source' => 'guid',
'target' => 'guid',
'unique' => 1,
),
4 => array(
'source' => 'description',
'target' => 'field_feed_item_description',
'unique' => FALSE,
),
),
'input_format' => 'filtered_html',
'author' => 0,
),
),
'content_type' => 'feed',
'update' => 0,
'import_period' => '1800',
'expire_period' => 3600,
'import_on_create' => 1,
'process_in_background' => FALSE,
);
$export['feed'] = $feeds_importer;
$feeds_importer = new stdClass;
$feeds_importer->disabled = FALSE; /* Edit this to true to make a default feeds_importer disabled initially */
$feeds_importer->api_version = 1;
$feeds_importer->id = 'opml';
$feeds_importer->config = array(
'name' => 'OPML import',
'description' => 'Import subscriptions from OPML files. Use together with "Feed" configuration.',
'fetcher' => array(
'plugin_key' => 'FeedsFileFetcher',
'config' => array(
'direct' => FALSE,
),
),
'parser' => array(
'plugin_key' => 'FeedsOPMLParser',
'config' => array(),
),
'processor' => array(
'plugin_key' => 'FeedsNodeProcessor',
'config' => array(
'content_type' => 'feed',
'update_existing' => 0,
'expire' => '-1',
'mappings' => array(
0 => array(
'source' => 'title',
'target' => 'title',
'unique' => FALSE,
),
1 => array(
'source' => 'xmlurl',
'target' => 'feeds_source',
'unique' => 1,
),
),
),
),
'content_type' => '',
'update' => 0,
'import_period' => '-1',
'expire_period' => 3600,
'import_on_create' => 1,
);
$export['opml'] = $feeds_importer;
return $export;
}

View File

@@ -0,0 +1,25 @@
core = "7.x"
dependencies[] = "features"
dependencies[] = "feeds"
dependencies[] = "views"
description = "A news aggregator built with feeds, creates nodes from imported feed items. With OPML import."
features[ctools][] = "feeds:feeds_importer_default:1"
features[ctools][] = "views:views_default:3.0"
features[feeds_importer][] = "feed"
features[feeds_importer][] = "opml"
features[field][] = "node-feed_item-field_feed_item_description"
features[node][] = "feed"
features[node][] = "feed_item"
features[views_view][] = "feeds_defaults_feed_items"
files[] = "feeds_news.module"
files[] = "feeds_news.test"
name = "Feeds News"
package = "Feeds"
php = "5.2.4"
; Information added by drupal.org packaging script on 2012-10-24
version = "7.x-2.0-alpha7"
core = "7.x"
project = "feeds"
datestamp = "1351111319"

View File

@@ -0,0 +1,8 @@
<?php
/**
* @file
* Empty module file.
*/
include_once('feeds_news.features.inc');

View File

@@ -0,0 +1,127 @@
<?php
/**
* @file
* Tests for feeds_news feature.
*/
/**
* Test Feed configuration.
*/
class FeedsExamplesFeedTestCase extends FeedsWebTestCase {
public static function getInfo() {
return array(
'name' => 'Feature: Feed',
'description' => 'Test "Feed" default configuration.',
'group' => 'Feeds',
'dependencies' => array('features', 'views'),
);
}
public function setUp() {
parent::setUp(array('features', 'views', 'feeds_news'));
}
/**
* Run tests.
*/
public function test() {
$nid = $this->createFeedNode('feed', NULL, '', 'feed');
// Assert menu tabs for feed nodes does not show up on non-feed nodes.
$this->drupalGet("node/{$nid}/feed-items");
$this->assertResponse(200);
$not_feed_node = $this->drupalCreateNode();
$this->drupalGet("node/{$not_feed_node->nid}/feed-items");
$this->assertResponse(404);
// Assert results.
$count = db_query("SELECT COUNT(*) FROM {node} WHERE type = 'feed_item'")->fetchField();
$this->assertEqual($count, 10, 'Found the correct number of feed item nodes in database.');
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node'")->fetchField();
$this->assertEqual($count, 10, 'Found the correct number of records in feeds_item.');
$count = db_query("SELECT COUNT(*) FROM {node} WHERE title = 'Open Atrium Translation Workflow: Two Way Translation Updates'")->fetchField();
$this->assertEqual($count, 1, 'Found title.');
$count = db_query("SELECT COUNT(*) FROM {node} WHERE title = 'Week in DC Tech: October 5th Edition'")->fetchField();
$this->assertEqual($count, 1, 'Found title.');
$count = db_query("SELECT COUNT(*) FROM {node} WHERE title = 'Integrating the Siteminder Access System in an Open Atrium-based Intranet'")->fetchField();
$this->assertEqual($count, 1, 'Found title.');
$count = db_query("SELECT COUNT(*) FROM {node} WHERE title = 'Scaling the Open Atrium UI'")->fetchField();
$this->assertEqual($count, 1, 'Found title.');
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node' AND url = 'http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating'")->fetchField();
$this->assertEqual($count, 1, 'Found feed_node_item record.');
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node' AND url = 'http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition'")->fetchField();
$this->assertEqual($count, 1, 'Found feed_node_item record.');
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node' AND guid = '974 at http://developmentseed.org'")->fetchField();
$this->assertEqual($count, 1, 'Found feed_node_item record.');
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node' AND guid = '970 at http://developmentseed.org'")->fetchField();
$this->assertEqual($count, 1, 'Found feed_node_item record.');
// Remove all items
$this->drupalPost("node/$nid/delete-items", array(), 'Delete');
$this->assertText('Deleted 10 nodes');
// Import again.
$this->drupalPost("node/$nid/import", array(), 'Import');
$this->assertText('Created 10 nodes');
// Delete and assert all items gone.
$this->drupalPost("node/$nid/delete-items", array(), 'Delete');
$count = db_query("SELECT COUNT(*) FROM {node} WHERE type = 'feed_item'")->fetchField();
$this->assertEqual($count, 0, 'Found the correct number of feed item nodes in database.');
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node'")->fetchField();
$this->assertEqual($count, 0, 'Found the correct number of records in feeds_item.');
// Create a batch of nodes.
$this->createFeedNodes('feed', 10, 'feed');
$count = db_query("SELECT COUNT(*) FROM {node} WHERE type = 'feed_item'")->fetchField();
$this->assertEqual($count, 100, 'Imported 100 nodes.');
$count = db_query("SELECT COUNT(*) FROM {feeds_item} WHERE entity_type = 'node'")->fetchField();
$this->assertEqual($count, 100, 'Found 100 records in feeds_item.');
}
}
/**
* Test OPML import configuration.
*/
class FeedsExamplesOPMLTestCase extends FeedsWebTestCase {
public static function getInfo() {
return array(
'name' => 'Feature: OPML import',
'description' => 'Test "OPML import" default configuration.',
'group' => 'Feeds',
);
}
/**
* Enable feeds_news feature.
*/
public function setUp() {
parent::setUp(array('feeds_news'));
}
/**
* Run tests.
*/
public function test() {
// Import OPML and assert.
$file = $this->generateOPML();
$this->importFile('opml', $file);
$this->assertText('Created 3 nodes');
$count = db_query("SELECT COUNT(*) FROM {feeds_source}")->fetchField();
$this->assertEqual($count, 4, 'Found correct number of items.');
// Import a feed and then delete all items from it.
$this->drupalPost('node/1/import', array(), 'Import');
$this->assertText('Created 10 nodes');
$this->drupalPost('node/1/delete-items', array(), 'Delete');
$this->assertText('Deleted 10 nodes');
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* @file
* feeds_news.views_default.inc
*/
/**
* Implementation of hook_views_default_views().
*/
function feeds_news_views_default_views() {
$export = array();
$view = new view;
$view->name = 'feeds_defaults_feed_items';
$view->description = 'Show feed items for a feed node. Use together with default importer configuration "Feed".';
$view->tag = 'Feeds defaults';
$view->base_table = 'node';
$view->human_name = '';
$view->core = 0;
$view->api_version = '3.0-alpha1';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
/* Display: Defaults */
$handler = $view->new_display('default', 'Defaults', 'default');
$handler->display->display_options['access']['type'] = 'perm';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['query']['options']['query_comment'] = FALSE;
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['style_plugin'] = 'default';
$handler->display->display_options['row_plugin'] = 'node';
$handler->display->display_options['row_options']['links'] = 1;
$handler->display->display_options['row_options']['comments'] = 0;
/* No results behavior: Global: Text area */
$handler->display->display_options['empty']['text']['id'] = 'area';
$handler->display->display_options['empty']['text']['table'] = 'views';
$handler->display->display_options['empty']['text']['field'] = 'area';
$handler->display->display_options['empty']['text']['empty'] = FALSE;
$handler->display->display_options['empty']['text']['content'] = 'There are no items for this feed at the moment.';
$handler->display->display_options['empty']['text']['format'] = '1';
/* Relationship: Feeds item: Owner feed */
$handler->display->display_options['relationships']['feed_nid_1']['id'] = 'feed_nid_1';
$handler->display->display_options['relationships']['feed_nid_1']['table'] = 'feeds_item';
$handler->display->display_options['relationships']['feed_nid_1']['field'] = 'feed_nid';
$handler->display->display_options['relationships']['feed_nid_1']['required'] = 1;
/* Sort criterion: Content: Post date */
$handler->display->display_options['sorts']['created']['id'] = 'created';
$handler->display->display_options['sorts']['created']['table'] = 'node';
$handler->display->display_options['sorts']['created']['field'] = 'created';
$handler->display->display_options['sorts']['created']['order'] = 'DESC';
/* Contextual filter: Content: Nid */
$handler->display->display_options['arguments']['nid']['id'] = 'nid';
$handler->display->display_options['arguments']['nid']['table'] = 'node';
$handler->display->display_options['arguments']['nid']['field'] = 'nid';
$handler->display->display_options['arguments']['nid']['relationship'] = 'feed_nid_1';
$handler->display->display_options['arguments']['nid']['default_action'] = 'not found';
$handler->display->display_options['arguments']['nid']['title_enable'] = 1;
$handler->display->display_options['arguments']['nid']['title'] = 'Articles from %1';
$handler->display->display_options['arguments']['nid']['default_argument_type'] = 'fixed';
$handler->display->display_options['arguments']['nid']['summary']['format'] = 'default_summary';
$handler->display->display_options['arguments']['nid']['specify_validation'] = 1;
$handler->display->display_options['arguments']['nid']['validate']['type'] = 'node';
$handler->display->display_options['arguments']['nid']['validate_options']['types'] = array(
'feed' => 'feed',
);
$handler->display->display_options['arguments']['nid']['break_phrase'] = 0;
$handler->display->display_options['arguments']['nid']['not'] = 0;
/* Filter criterion: Content: Type */
$handler->display->display_options['filters']['type']['id'] = 'type';
$handler->display->display_options['filters']['type']['table'] = 'node';
$handler->display->display_options['filters']['type']['field'] = 'type';
$handler->display->display_options['filters']['type']['value'] = array(
'feed_item' => 'feed_item',
);
$handler->display->display_options['filters']['type']['expose']['operator'] = FALSE;
/* Display: Page */
$handler = $view->new_display('page', 'Page', 'page_1');
$handler->display->display_options['path'] = 'node/%/feed-items';
$handler->display->display_options['menu']['type'] = 'tab';
$handler->display->display_options['menu']['title'] = 'View items';
$handler->display->display_options['menu']['weight'] = '-9';
$translatables['feeds_defaults_feed_items'] = array(
t('Defaults'),
t('more'),
t('Apply'),
t('Reset'),
t('Sort by'),
t('Asc'),
t('Desc'),
t('Items per page'),
t('- All -'),
t('Offset'),
t('There are no items for this feed at the moment.'),
t('Owner feed'),
t('All'),
t('Articles from %1'),
t('Page'),
);
$export['feeds_defaults_feed_items'] = $view;
return $export;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
/* Feeds admin overview form. */
table.feeds-admin-importers thead th {
border: none;
}
table.feeds-admin-importers td.disabled {
color: #aaa;
}
table.feeds-admin-importers tr.disabled.odd,
table.feeds-admin-importers tr.disabled.even {
border-color: #eee;
}
table.feeds-admin-importers tr.disabled.odd {
background-color: #f5f5f5;
}
/* Feeds edit form layout. */
div.feeds-settings {
}
div.left-bar {
float: left;
position: relative;
width: 240px;
border-right: 1px solid #DDD;
padding: 10px 10px 0 0;
}
div.configuration {
padding: 10px 0 0 250px;
margin-left: -240px;
}
div.configuration-squeeze {
margin-left: 250px;
}
/* Container theming. */
div.feeds-container {
}
div.feeds-container h4 {
font-size: 1.2em;
font-weight: bold;
}
div.feeds-container.plain {
background-color: #EEE;
border-bottom: 1px solid #DDD;
border-top: 2px solid #DDD;
padding: 5px;
margin: 10px 0;
}
div.feeds-container.plain h4 {
font-size: 1.0em;
margin: 0;
padding: 0;
}
div.feeds-container-body p {
padding: 5px 0;
margin: 0;
}
div.feeds-container-body div.item-list ul {
margin: 0;
}
div.feeds-container-body div.item-list ul li {
list-style-type: none;
margin: 0;
padding: 0;
background-image: none;
}
ul.container-actions {
font-family: Arial, Helvetica;
float: right;
margin: 0;
}
ul.container-actions li {
list-style-type: none;
text-align: right;
background-image: none;
margin: 0;
padding: 0;
}
ul.container-actions .form-item,
ul.container-actions li form,
ul.container-actions li form input {
padding: 0;
margin: 0;
display: inline;
}
/* Mapping form. */
#center table form {
margin: 0;
}

View File

@@ -0,0 +1,15 @@
name = Feeds Admin UI
description = Administrative UI for Feeds module.
package = Feeds
core = 7.x
dependencies[] = feeds
configure = admin/structure/feeds
files[] = feeds_ui.test
; Information added by drupal.org packaging script on 2012-10-24
version = "7.x-2.0-alpha7"
core = "7.x"
project = "feeds"
datestamp = "1351111319"

View File

@@ -0,0 +1,13 @@
<?php
/**
* @file
* Install, uninstall, and update functions for the feeds_ui module.
*/
/**
* Empty update function to trigger a menu rebuild.
*/
function feeds_ui_update_7000() {
// Do nothing.
}

View File

@@ -0,0 +1,62 @@
Drupal.behaviors.feeds = function() {
// Hide text in specific input fields.
$('.hide-text-on-focus').focus(function() {
$(this).val('');
});
// Hide submit buttons of .feeds-ui-hidden-submit class.
$('input.form-submit.feeds-ui-hidden-submit').hide();
/**
* Tune checkboxes on mapping forms.
* @see feeds_ui_mapping_form() in feeds_ui.admin.inc
*/
// Attach submit behavior to elements with feeds-ui-trigger-submit class.
$('.feeds-ui-trigger-submit').click(function() {
// Use click, not form.submit() - submit() would use the wrong submission
// handler.
$('input.form-submit.feeds-ui-hidden-submit').click();
});
// Replace checkbox with .feeds-ui-checkbox-link class with a link.
$('.feeds-ui-checkbox-link:not(.processed)').each(function(i) {
$(this).addClass('processed').after(
'<a href="#" onclick="return false;" class="feeds-ui-trigger-remove">' + $('label', this).text() + '</a>'
).hide();
});
// Check the box and then submit.
$('.feeds-ui-trigger-remove').click(function() {
// Use click, not form.submit() - submit() would use the wrong submission
// handler.
$(this).prev().children().children().children().attr('checked', 1);
$('input.form-submit.feeds-ui-hidden-submit').click();
});
// Replace radio with .feeds-ui-radio-link class with a link.
$('.feeds-ui-radio-link:not(.processed)').parent().each(function(i) {
checked = '';
if ($(this).children('input').attr('checked')) {
checked = ' checked';
}
$(this).addClass('processed').after(
'<a href="#" onclick="return false;" class="feeds-ui-check-submit' + checked + '" id="' + $(this).children('input').attr('id') + '">' + $(this).parent().text() + '</a>'
);
$(this).hide();
});
// Hide the the radio that is selected.
$('.feeds-ui-check-submit.checked').parent().hide();
// Check the radio and then submit.
$('.feeds-ui-check-submit').click(function() {
// Use click, not form.submit() - submit() would use the wrong submission
// handler.
$('#' + $(this).attr('id')).attr('checked', 1);
$('input.form-submit.feeds-ui-hidden-submit').click();
});
};

View File

@@ -0,0 +1,137 @@
<?php
/**
* @file
*/
/**
* Implements hook_help().
*/
function feeds_ui_help($path, $arg) {
switch ($path) {
case 'admin/structure/feeds':
$output = '<p>' . t('Create one or more Feed importers for pulling content into Drupal. You can use these importers from the <a href="@import">Import</a> page or - if you attach them to a content type - simply by creating a node from that content type.', array('@import' => url('import'))) . '</p>';
return $output;
}
}
/**
* Implements hook_menu().
*/
function feeds_ui_menu() {
$items = array();
$items['admin/structure/feeds'] = array(
'title' => 'Feeds importers',
'description' => 'Configure one or more Feeds importers to aggregate RSS and Atom feeds, import CSV files or more.',
'page callback' => 'drupal_get_form',
'page arguments' => array('feeds_ui_overview_form'),
'access arguments' => array('administer feeds'),
'file' => 'feeds_ui.admin.inc',
);
$items['admin/structure/feeds/create'] = array(
'title' => 'Add importer',
'page callback' => 'drupal_get_form',
'page arguments' => array('feeds_ui_create_form'),
'access arguments' => array('administer feeds'),
'file' => 'feeds_ui.admin.inc',
'type' => MENU_LOCAL_ACTION,
);
$items['admin/structure/feeds/%feeds_importer'] = array(
'title callback' => 'feeds_ui_importer_title',
'title arguments' => array(3),
'page callback' => 'feeds_ui_edit_page',
'page arguments' => array(3),
'access arguments' => array('administer feeds'),
'file' => 'feeds_ui.admin.inc',
);
$items['admin/structure/feeds/%feeds_importer/edit'] = array(
'title' => 'Edit',
'page callback' => 'feeds_ui_edit_page',
'page arguments' => array(3),
'access arguments' => array('administer feeds'),
'file' => 'feeds_ui.admin.inc',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => 1,
);
$items['admin/structure/feeds/%feeds_importer/export'] = array(
'title' => 'Export',
'page callback' => 'drupal_get_form',
'page arguments' => array('feeds_ui_export_form', 3),
'access arguments' => array('administer feeds'),
'file' => 'feeds_ui.admin.inc',
'type' => MENU_LOCAL_TASK,
'weight' => 2,
);
$items['admin/structure/feeds/%feeds_importer/clone'] = array(
'title' => 'Clone',
'page callback' => 'drupal_get_form',
'page arguments' => array('feeds_ui_create_form', 3),
'access arguments' => array('administer feeds'),
'file' => 'feeds_ui.admin.inc',
'type' => MENU_LOCAL_TASK,
'weight' => 3,
);
$items['admin/structure/feeds/%feeds_importer/delete'] = array(
'title' => 'Delete',
'page callback' => 'drupal_get_form',
'page arguments' => array('feeds_ui_delete_form', 3),
'access arguments' => array('administer feeds'),
'file' => 'feeds_ui.admin.inc',
'type' => MENU_LOCAL_TASK,
'weight' => 4,
);
return $items;
}
/**
* Implements hook_theme().
*/
function feeds_ui_theme() {
return array(
'feeds_ui_overview_form' => array(
'render element' => 'form',
'file' => 'feeds_ui.admin.inc',
),
'feeds_ui_mapping_form' => array(
'render element' => 'form',
'file' => 'feeds_ui.admin.inc',
),
'feeds_ui_edit_page' => array(
'variables' => array('info' => NULL, 'active' => NULL),
'file' => 'feeds_ui.admin.inc',
),
'feeds_ui_plugin_form' => array(
'render element' => 'form',
'file' => 'feeds_ui.admin.inc',
),
'feeds_ui_container' => array(
'variables' => array('container' => NULL),
'file' => 'feeds_ui.admin.inc',
),
);
}
/**
* Implements hook_admin_menu_map().
*/
function feeds_ui_admin_menu_map() {
// Add awareness to the administration menu of the various importers so they
// are included in the dropdown menu.
if (!user_access('administer feeds')) {
return;
}
$map['admin/structure/feeds/%feeds_importer'] = array(
'parent' => 'admin/structure/feeds',
'arguments' => array(
array('%feeds_importer' => feeds_enabled_importers()),
),
);
return $map;
}
/**
* Title callback for importers.
*/
function feeds_ui_importer_title($importer) {
return t('@importer', array('@importer' => $importer->config['name']));
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* @file
* Tests for Feeds Admin UI module.
*/
/**
* Test basic Feeds UI functionality.
*/
class FeedsUIUserInterfaceTestCase extends FeedsWebTestCase {
public static function getInfo() {
return array(
'name' => 'Feeds UI user interface',
'description' => 'Tests Feeds Admin UI module\'s GUI.',
'group' => 'Feeds',
);
}
/**
* UI functionality tests on
* feeds_ui_overview(),
* feeds_ui_create_form(),
* Change plugins on feeds_ui_edit_page().
*/
public function testEditFeedConfiguration() {
// Create an importer.
$this->createImporterConfiguration('Test feed', 'test_feed');
// Assert UI elements.
$this->drupalGet('admin/structure/feeds/test_feed');
$this->assertText('Basic settings');
$this->assertText('Fetcher');
$this->assertText('HTTP Fetcher');
$this->assertText('Parser');
$this->assertText('Common syndication parser');
$this->assertText('Processor');
$this->assertText('Node processor');
$this->assertText('Getting started');
$this->assertRaw('admin/structure/feeds/test_feed/settings');
$this->assertRaw('admin/structure/feeds/test_feed/settings/FeedsNodeProcessor');
$this->assertRaw('admin/structure/feeds/test_feed/fetcher');
$this->assertRaw('admin/structure/feeds/test_feed/parser');
$this->assertRaw('admin/structure/feeds/test_feed/processor');
$this->drupalGet('import');
$this->assertText('Basic page');
// Select some other plugins.
$this->drupalGet('admin/structure/feeds/test_feed');
$this->clickLink('Change', 0);
$this->assertText('Select a fetcher');
$edit = array(
'plugin_key' => 'FeedsFileFetcher',
);
$this->drupalPost('admin/structure/feeds/test_feed/fetcher', $edit, 'Save');
$this->clickLink('Change', 1);
$this->assertText('Select a parser');
$edit = array(
'plugin_key' => 'FeedsCSVParser',
);
$this->drupalPost('admin/structure/feeds/test_feed/parser', $edit, 'Save');
$this->clickLink('Change', 2);
$this->assertText('Select a processor');
$edit = array(
'plugin_key' => 'FeedsUserProcessor',
);
$this->drupalPost('admin/structure/feeds/test_feed/processor', $edit, 'Save');
// Assert changed configuration.
$this->assertPlugins('test_feed', 'FeedsFileFetcher', 'FeedsCSVParser', 'FeedsUserProcessor');
// Delete importer.
$this->drupalPost('admin/structure/feeds/test_feed/delete', array(), 'Delete');
$this->drupalGet('import');
$this->assertNoText('Test feed');
// Create the same importer again.
$this->createImporterConfiguration('Test feed', 'test_feed');
// Test basic settings settings.
$edit = array(
'name' => 'Syndication feed',
'content_type' => 'page',
'import_period' => 3600,
);
$this->drupalPost('admin/structure/feeds/test_feed/settings', $edit, 'Save');
// Assert results of change.
$this->assertText('Syndication feed');
$this->assertText('Your changes have been saved.');
$this->assertText('Attached to: Basic page');
$this->assertText('Periodic import: every 1 hour');
$this->drupalGet('admin/structure/feeds');
$this->assertLink('Basic page');
// Configure processor.
$edit = array(
'content_type' => 'article',
);
$this->drupalPost('admin/structure/feeds/test_feed/settings/FeedsNodeProcessor', $edit, 'Save');
$this->assertFieldByName('content_type', 'article');
// Create a feed node.
$edit = array(
'title' => 'Development Seed',
'feeds[FeedsHTTPFetcher][source]' => $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'feeds') . '/tests/feeds/developmentseed.rss2',
);
$this->drupalPost('node/add/page', $edit, 'Save');
$this->assertText('Basic page Development Seed has been created.');
// @todo Refreshing/deleting feed items. Needs to live in feeds.test
}
}

View File

@@ -0,0 +1,291 @@
<?php
/**
* @file
* FeedsConfigurable and helper functions.
*/
/**
* Used when an object does not exist in the DB or code but should.
*/
class FeedsNotExistingException extends Exception {
}
/**
* Base class for configurable classes. Captures configuration handling, form
* handling and distinguishes between in-memory configuration and persistent
* configuration.
*/
abstract class FeedsConfigurable {
// Holds the actual configuration information.
protected $config;
// A unique identifier for the configuration.
protected $id;
/*
CTools export type of this object.
@todo Should live in FeedsImporter. Not all child classes
of FeedsConfigurable are exportable. Same goes for $disabled.
Export type can be one of
FEEDS_EXPORT_NONE - the configurable only exists in memory
EXPORT_IN_DATABASE - the configurable is defined in the database.
EXPORT_IN_CODE - the configurable is defined in code.
EXPORT_IN_CODE | EXPORT_IN_DATABASE - the configurable is defined in code, but
overridden in the database.*/
protected $export_type;
/**
* CTools export enabled status of this object.
*/
protected $disabled;
/**
* Instantiate a FeedsConfigurable object.
*
* Don't use directly, use feeds_importer() or feeds_plugin()
* instead.
*/
public static function instance($class, $id) {
// This is useful at least as long as we're developing.
if (empty($id)) {
throw new Exception(t('Empty configuration identifier.'));
}
static $instances = array();
if (!isset($instances[$class][$id])) {
$instances[$class][$id] = new $class($id);
}
return $instances[$class][$id];
}
/**
* Constructor, set id and load default configuration.
*/
protected function __construct($id) {
// Set this object's id.
$this->id = $id;
// Per default we assume that a Feeds object is not saved to
// database nor is it exported to code.
$this->export_type = FEEDS_EXPORT_NONE;
// Make sure configuration is populated.
$this->config = $this->configDefaults();
$this->disabled = FALSE;
}
/**
* Override magic method __isset(). This is needed due to overriding __get().
*/
public function __isset($name) {
return isset($this->$name) ? TRUE : FALSE;
}
/**
* Determine whether this object is persistent and enabled. I. e. it is
* defined either in code or in the database and it is enabled.
*/
public function existing() {
if ($this->export_type == FEEDS_EXPORT_NONE) {
throw new FeedsNotExistingException(t('Object is not persistent.'));
}
if ($this->disabled) {
throw new FeedsNotExistingException(t('Object is disabled.'));
}
return $this;
}
/**
* Save a configuration. Concrete extending classes must implement a save
* operation.
*/
public abstract function save();
/**
* Copy a configuration.
*/
public function copy(FeedsConfigurable $configurable) {
$this->setConfig($configurable->config);
}
/**
* Set configuration.
*
* @param $config
* Array containing configuration information. Config array will be filtered
* by the keys returned by configDefaults() and populated with default
* values that are not included in $config.
*/
public function setConfig($config) {
$defaults = $this->configDefaults();
$this->config = array_intersect_key($config, $defaults) + $defaults;
}
/**
* Similar to setConfig but adds to existing configuration.
*
* @param $config
* Array containing configuration information. Will be filtered by the keys
* returned by configDefaults().
*/
public function addConfig($config) {
$this->config = is_array($this->config) ? array_merge($this->config, $config) : $config;
$default_keys = $this->configDefaults();
$this->config = array_intersect_key($this->config, $default_keys);
}
/**
* Override magic method __get(). Make sure that $this->config goes through
* getConfig().
*/
public function __get($name) {
if ($name == 'config') {
return $this->getConfig();
}
return isset($this->$name) ? $this->$name : NULL;
}
/**
* Implements getConfig().
*
* Return configuration array, ensure that all default values are present.
*/
public function getConfig() {
$defaults = $this->configDefaults();
return $this->config + $defaults;
}
/**
* Return default configuration.
*
* @todo rename to getConfigDefaults().
*
* @return
* Array where keys are the variable names of the configuration elements and
* values are their default values.
*/
public function configDefaults() {
return array();
}
/**
* Return configuration form for this object. The keys of the configuration
* form must match the keys of the array returned by configDefaults().
*
* @return
* FormAPI style form definition.
*/
public function configForm(&$form_state) {
return array();
}
/**
* Validation handler for configForm().
*
* Set errors with form_set_error().
*
* @param $values
* An array that contains the values entered by the user through configForm.
*/
public function configFormValidate(&$values) {
}
/**
* Submission handler for configForm().
*
* @param $values
*/
public function configFormSubmit(&$values) {
$this->addConfig($values);
$this->save();
drupal_set_message(t('Your changes have been saved.'));
feeds_cache_clear(FALSE);
}
}
/**
* Config form wrapper. Use to render the configuration form of
* a FeedsConfigurable object.
*
* @param $configurable
* FeedsConfigurable object.
* @param $form_method
* The form method that should be rendered.
*
* @return
* Config form array if available. NULL otherwise.
*/
function feeds_get_form($configurable, $form_method) {
if (method_exists($configurable, $form_method)) {
return drupal_get_form(get_class($configurable) . '_feeds_form', $configurable, $form_method);
}
}
/**
* Config form callback. Don't call directly, but use
* feeds_get_form($configurable, 'method') instead.
*
* @param
* FormAPI $form_state.
* @param
* FeedsConfigurable object.
* @param
* The object to perform the save() operation on.
* @param $form_method
* The $form_method that should be rendered.
*/
function feeds_form($form, &$form_state, $configurable, $form_method) {
$form = $configurable->$form_method($form_state);
$form['#configurable'] = $configurable;
$form['#feeds_form_method'] = $form_method;
$form['#validate'] = array('feeds_form_validate');
$form['#submit'] = array('feeds_form_submit');
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#weight' => 100,
);
return $form;
}
/**
* Validation handler for feeds_form().
*/
function feeds_form_validate($form, &$form_state) {
_feeds_form_helper($form, $form_state, 'Validate');
}
/**
* Submit handler for feeds_form().
*/
function feeds_form_submit($form, &$form_state) {
_feeds_form_helper($form, $form_state, 'Submit');
}
/**
* Helper for Feeds validate and submit callbacks.
*/
function _feeds_form_helper($form, &$form_state, $action) {
$method = $form['#feeds_form_method'] . $action;
$class = get_class($form['#configurable']);
$id = $form['#configurable']->id;
// Re-initialize the configurable object. Using feeds_importer() and
// feeds_plugin() will ensure that we're using the same instance. We can't
// reuse the previous form instance because feeds_importer() is used to save.
// This will re-initialize all of the plugins anyway, causing some tricky
// saving issues in certain cases.
// See http://drupal.org/node/1672880.
if ($class == variable_get('feeds_importer_class', 'FeedsImporter')) {
$form['#configurable'] = feeds_importer($id);
}
else {
$form['#configurable'] = feeds_plugin($class, $id);
}
if (method_exists($form['#configurable'], $method)) {
$form['#configurable']->$method($form_state['values']);
}
}

View File

@@ -0,0 +1,333 @@
<?php
/**
* @file
* FeedsImporter class and related.
*/
/**
* A FeedsImporter object describes how an external source should be fetched,
* parsed and processed. Feeds can manage an arbitrary amount of importers.
*
* A FeedsImporter holds a pointer to a FeedsFetcher, a FeedsParser and a
* FeedsProcessor plugin. It further contains the configuration for itself and
* each of the three plugins.
*
* Its most important responsibilities are configuration management, interfacing
* with the job scheduler and expiring of all items produced by this
* importer.
*
* When a FeedsImporter is instantiated, it loads its configuration. Then it
* instantiates one fetcher, one parser and one processor plugin depending on
* the configuration information. After instantiating them, it sets them to
* the configuration information it holds for them.
*/
class FeedsImporter extends FeedsConfigurable {
// Every feed has a fetcher, a parser and a processor.
// These variable names match the possible return values of
// FeedsPlugin::typeOf().
protected $fetcher, $parser, $processor;
// This array defines the variable names of the plugins above.
protected $plugin_types = array('fetcher', 'parser', 'processor');
/**
* Instantiate class variables, initialize and configure
* plugins.
*/
protected function __construct($id) {
parent::__construct($id);
// Try to load information from database.
$this->load();
// Instantiate fetcher, parser and processor, set their configuration if
// stored info is available.
foreach ($this->plugin_types as $type) {
$plugin = feeds_plugin($this->config[$type]['plugin_key'], $this->id);
if (isset($this->config[$type]['config'])) {
$plugin->setConfig($this->config[$type]['config']);
}
$this->$type = $plugin;
}
}
/**
* Remove items older than $time.
*
* @param $time
* All items older than REQUEST_TIME - $time will be deleted. If not
* given, internal processor settings will be used.
*
* @return
* FEEDS_BATCH_COMPLETE if the expiry process finished. A decimal between
* 0.0 and 0.9 periodic if expiry is still in progress.
*
* @throws
* Throws Exception if an error occurs when expiring items.
*/
public function expire($time = NULL) {
return $this->processor->expire($time);
}
/**
* Schedule all periodic tasks for this importer.
*/
public function schedule() {
$this->scheduleExpire();
}
/**
* Schedule expiry of items.
*/
public function scheduleExpire() {
$job = array(
'type' => $this->id,
'period' => 0,
'periodic' => TRUE,
);
if (FEEDS_EXPIRE_NEVER != $this->processor->expiryTime()) {
$job['period'] = 3600;
JobScheduler::get('feeds_importer_expire')->set($job);
}
else {
JobScheduler::get('feeds_importer_expire')->remove($job);
}
}
/**
* Report how many items *should* be created on one page load by this
* importer.
*
* Note:
*
* It depends on whether parser implements batching if this limit is actually
* respected. Further, if no limit is reported it doesn't mean that the
* number of items that can be created on one page load is actually without
* limit.
*
* @return
* A positive number defining the number of items that can be created on
* one page load. 0 if this number is unlimited.
*/
public function getLimit() {
return $this->processor->getLimit();
}
/**
* Save configuration.
*/
public function save() {
$save = new stdClass();
$save->id = $this->id;
$save->config = $this->getConfig();
if ($config = db_query("SELECT config FROM {feeds_importer} WHERE id = :id", array(':id' => $this->id))->fetchField()) {
drupal_write_record('feeds_importer', $save, 'id');
// Only rebuild menu if content_type has changed. Don't worry about
// rebuilding menus when creating a new importer since it will default
// to the standalone page.
$config = unserialize($config);
if ($config['content_type'] != $save->config['content_type']) {
variable_set('menu_rebuild_needed', TRUE);
}
}
else {
drupal_write_record('feeds_importer', $save);
}
}
/**
* Load configuration and unpack.
*/
public function load() {
ctools_include('export');
if ($config = ctools_export_load_object('feeds_importer', 'conditions', array('id' => $this->id))) {
$config = array_shift($config);
$this->export_type = $config->export_type;
$this->disabled = isset($config->disabled) ? $config->disabled : FALSE;
$this->config = $config->config;
return TRUE;
}
return FALSE;
}
/**
* Delete configuration. Removes configuration information
* from database, does not delete configuration itself.
*/
public function delete() {
db_delete('feeds_importer')
->condition('id', $this->id)
->execute();
$job = array(
'type' => $this->id,
'id' => 0,
);
if ($this->export_type & EXPORT_IN_CODE) {
feeds_reschedule($this->id);
}
else {
JobScheduler::get('feeds_importer_expire')->remove($job);
}
}
/**
* Set plugin.
*
* @param $plugin_key
* A fetcher, parser or processor plugin.
*
* @todo Error handling, handle setting to the same plugin.
*/
public function setPlugin($plugin_key) {
// $plugin_type can be either 'fetcher', 'parser' or 'processor'
if ($plugin_type = FeedsPlugin::typeOf($plugin_key)) {
if ($plugin = feeds_plugin($plugin_key, $this->id)) {
// Unset existing plugin, switch to new plugin.
unset($this->$plugin_type);
$this->$plugin_type = $plugin;
// Set configuration information, blow away any previous information on
// this spot.
$this->config[$plugin_type] = array('plugin_key' => $plugin_key);
}
}
}
/**
* Copy a FeedsImporter configuration into this importer.
*
* @param FeedsImporter $importer
* The feeds importer object to copy from.
*/
public function copy(FeedsConfigurable $configurable) {
parent::copy($configurable);
if ($configurable instanceof FeedsImporter) {
// Instantiate new fetcher, parser and processor and initialize their
// configurations.
foreach ($this->plugin_types as $plugin_type) {
$this->setPlugin($configurable->config[$plugin_type]['plugin_key']);
$this->$plugin_type->setConfig($configurable->config[$plugin_type]['config']);
}
}
}
/**
* Get configuration of this feed.
*/
public function getConfig() {
foreach (array('fetcher', 'parser', 'processor') as $type) {
$this->config[$type]['config'] = $this->$type->getConfig();
}
return parent::getConfig();
}
/**
* Return defaults for feed configuration.
*/
public function configDefaults() {
return array(
'name' => '',
'description' => '',
'fetcher' => array(
'plugin_key' => 'FeedsHTTPFetcher',
),
'parser' => array(
'plugin_key' => 'FeedsSyndicationParser',
),
'processor' => array(
'plugin_key' => 'FeedsNodeProcessor',
),
'content_type' => '',
'update' => 0,
'import_period' => 1800, // Refresh every 30 minutes by default.
'expire_period' => 3600, // Expire every hour by default, this is a hidden setting.
'import_on_create' => TRUE, // Import on submission.
'process_in_background' => FALSE,
);
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$config = $this->getConfig();
$form = array();
$form['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#description' => t('A human readable name of this importer.'),
'#default_value' => $config['name'],
'#required' => TRUE,
);
$form['description'] = array(
'#type' => 'textfield',
'#title' => t('Description'),
'#description' => t('A description of this importer.'),
'#default_value' => $config['description'],
);
$node_types = node_type_get_names();
array_walk($node_types, 'check_plain');
$form['content_type'] = array(
'#type' => 'select',
'#title' => t('Attach to content type'),
'#description' => t('If "Use standalone form" is selected a source is imported by using a form under !import_form.
If a content type is selected a source is imported by creating a node of that content type.',
array('!import_form' => l(url('import', array('absolute' => TRUE)), 'import', array('attributes' => array('target' => '_new'))))),
'#options' => array('' => t('Use standalone form')) + $node_types,
'#default_value' => $config['content_type'],
);
$cron_required = ' ' . l(t('Requires cron to be configured.'), 'http://drupal.org/cron', array('attributes' => array('target' => '_new')));
$period = drupal_map_assoc(array(900, 1800, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2419200), 'format_interval');
foreach ($period as &$p) {
$p = t('Every !p', array('!p' => $p));
}
$period = array(
FEEDS_SCHEDULE_NEVER => t('Off'),
0 => t('As often as possible'),
) + $period;
$form['import_period'] = array(
'#type' => 'select',
'#title' => t('Periodic import'),
'#options' => $period,
'#description' => t('Choose how often a source should be imported periodically.') . $cron_required,
'#default_value' => $config['import_period'],
);
$form['import_on_create'] = array(
'#type' => 'checkbox',
'#title' => t('Import on submission'),
'#description' => t('Check if import should be started at the moment a standalone form or node form is submitted.'),
'#default_value' => $config['import_on_create'],
);
$form['process_in_background'] = array(
'#type' => 'checkbox',
'#title' => t('Process in background'),
'#description' => t('For very large imports. If checked, import and delete tasks started from the web UI will be handled by a cron task in the background rather than by the browser. This does not affect periodic imports, they are handled by a cron task in any case.') . $cron_required,
'#default_value' => $config['process_in_background'],
);
return $form;
}
/**
* Reschedule if import period changes.
*/
public function configFormSubmit(&$values) {
if ($this->config['import_period'] != $values['import_period']) {
feeds_reschedule($this->id);
}
parent::configFormSubmit($values);
}
}
/**
* Helper, see FeedsDataProcessor class.
*/
function feeds_format_expire($timestamp) {
if ($timestamp == FEEDS_EXPIRE_NEVER) {
return t('Never');
}
return t('after !time', array('!time' => format_interval($timestamp)));
}

View File

@@ -0,0 +1,725 @@
<?php
/**
* @file
* Definition of FeedsSourceInterface and FeedsSource class.
*/
/**
* Distinguish exceptions occuring when handling locks.
*/
class FeedsLockException extends Exception {}
/**
* Denote a import or clearing stage. Used for multi page processing.
*/
define('FEEDS_START', 'start_time');
define('FEEDS_FETCH', 'fetch');
define('FEEDS_PARSE', 'parse');
define('FEEDS_PROCESS', 'process');
define('FEEDS_PROCESS_CLEAR', 'process_clear');
/**
* Declares an interface for a class that defines default values and form
* descriptions for a FeedSource.
*/
interface FeedsSourceInterface {
/**
* Crutch: for ease of use, we implement FeedsSourceInterface for every
* plugin, but then we need to have a handle which plugin actually implements
* a source.
*
* @see FeedsPlugin class.
*
* @return
* TRUE if a plugin handles source specific configuration, FALSE otherwise.
*/
public function hasSourceConfig();
/**
* Return an associative array of default values.
*/
public function sourceDefaults();
/**
* Return a Form API form array that defines a form configuring values. Keys
* correspond to the keys of the return value of sourceDefaults().
*/
public function sourceForm($source_config);
/**
* Validate user entered values submitted by sourceForm().
*/
public function sourceFormValidate(&$source_config);
/**
* A source is being saved.
*/
public function sourceSave(FeedsSource $source);
/**
* A source is being deleted.
*/
public function sourceDelete(FeedsSource $source);
}
/**
* Status of an import or clearing operation on a source.
*/
class FeedsState {
/**
* Floating point number denoting the progress made. 0.0 meaning no progress
* 1.0 = FEEDS_BATCH_COMPLETE meaning finished.
*/
public $progress;
/**
* Used as a pointer to store where left off. Must be serializable.
*/
public $pointer;
/**
* Natural numbers denoting more details about the progress being made.
*/
public $total;
public $created;
public $updated;
public $deleted;
public $skipped;
public $failed;
/**
* Constructor, initialize variables.
*/
public function __construct() {
$this->progress = FEEDS_BATCH_COMPLETE;
$this->total =
$this->created =
$this->updated =
$this->deleted =
$this->skipped =
$this->failed = 0;
}
/**
* Safely report progress.
*
* When $total == $progress, the state of the task tracked by this state is
* regarded to be complete.
*
* Handles the following cases gracefully:
*
* - $total is 0
* - $progress is larger than $total
* - $progress approximates $total so that $finished rounds to 1.0
*
* @param $total
* A natural number that is the total to be worked off.
* @param $progress
* A natural number that is the progress made on $total.
*/
public function progress($total, $progress) {
if ($progress > $total) {
$this->progress = FEEDS_BATCH_COMPLETE;
}
elseif ($total) {
$this->progress = $progress / $total;
if ($this->progress == FEEDS_BATCH_COMPLETE && $total != $progress) {
$this->progress = 0.99;
}
}
else {
$this->progress = FEEDS_BATCH_COMPLETE;
}
}
}
/**
* This class encapsulates a source of a feed. It stores where the feed can be
* found and how to import it.
*
* Information on how to import a feed is encapsulated in a FeedsImporter object
* which is identified by the common id of the FeedsSource and the
* FeedsImporter. More than one FeedsSource can use the same FeedsImporter
* therefore a FeedsImporter never holds a pointer to a FeedsSource object, nor
* does it hold any other information for a particular FeedsSource object.
*
* Classes extending FeedsPlugin can implement a sourceForm to expose
* configuration for a FeedsSource object. This is for instance how FeedsFetcher
* exposes a text field for a feed URL or how FeedsCSVParser exposes a select
* field for choosing between colon or semicolon delimiters.
*
* It is important that a FeedsPlugin does not directly hold information about
* a source but leave all storage up to FeedsSource. An instance of a
* FeedsPlugin class only exists once per FeedsImporter configuration, while an
* instance of a FeedsSource class exists once per feed_nid to be imported.
*
* As with FeedsImporter, the idea with FeedsSource is that it can be used
* without actually saving the object to the database.
*/
class FeedsSource extends FeedsConfigurable {
// Contains the node id of the feed this source info object is attached to.
// Equals 0 if not attached to any node - i. e. if used on a
// standalone import form within Feeds or by other API users.
protected $feed_nid;
// The FeedsImporter object that this source is expected to be used with.
protected $importer;
// A FeedsSourceState object holding the current import/clearing state of this
// source.
protected $state;
// Fetcher result, used to cache fetcher result when batching.
protected $fetcher_result;
// Timestamp when this source was imported the last time.
protected $imported;
/**
* Instantiate a unique object per class/id/feed_nid. Don't use
* directly, use feeds_source() instead.
*/
public static function instance($importer_id, $feed_nid) {
$class = variable_get('feeds_source_class', 'FeedsSource');
static $instances = array();
if (!isset($instances[$class][$importer_id][$feed_nid])) {
$instances[$class][$importer_id][$feed_nid] = new $class($importer_id, $feed_nid);
}
return $instances[$class][$importer_id][$feed_nid];
}
/**
* Constructor.
*/
protected function __construct($importer_id, $feed_nid) {
$this->feed_nid = $feed_nid;
$this->importer = feeds_importer($importer_id);
parent::__construct($importer_id);
$this->load();
}
/**
* Returns the FeedsImporter object that this source is expected to be used with.
*/
public function importer() {
return $this->importer;
}
/**
* Preview = fetch and parse a feed.
*
* @return
* FeedsParserResult object.
*
* @throws
* Throws Exception if an error occurs when fetching or parsing.
*/
public function preview() {
$result = $this->importer->fetcher->fetch($this);
$result = $this->importer->parser->parse($this, $result);
module_invoke_all('feeds_after_parse', $this, $result);
return $result;
}
/**
* Start importing a source.
*
* This method starts an import job. Depending on the configuration of the
* importer of this source, a Batch API job or a background job with Job
* Scheduler will be created.
*
* @throws Exception
* If processing in background is enabled, the first batch chunk of the
* import will be executed on the current page request. This means that this
* method may throw the same exceptions as FeedsSource::import().
*/
public function startImport() {
$config = $this->importer->getConfig();
if ($config['process_in_background']) {
$this->startBackgroundJob('import');
}
else {
$this->startBatchAPIJob(t('Importing'), 'import');
}
}
/**
* Start deleting all imported items of a source.
*
* This method starts a clear job. Depending on the configuration of the
* importer of this source, a Batch API job or a background job with Job
* Scheduler will be created.
*
* @throws Exception
* If processing in background is enabled, the first batch chunk of the
* clear task will be executed on the current page request. This means that
* this method may throw the same exceptions as FeedsSource::clear().
*/
public function startClear() {
$config = $this->importer->getConfig();
if ($config['process_in_background']) {
$this->startBackgroundJob('clear');
}
else {
$this->startBatchAPIJob(t('Deleting'), 'clear');
}
}
/**
* Schedule all periodic tasks for this source.
*/
public function schedule() {
$this->scheduleImport();
}
/**
* Schedule periodic or background import tasks.
*/
public function scheduleImport() {
// Check whether any fetcher is overriding the import period.
$period = $this->importer->config['import_period'];
$fetcher_period = $this->importer->fetcher->importPeriod($this);
if (is_numeric($fetcher_period)) {
$period = $fetcher_period;
}
$period = $this->progressImporting() === FEEDS_BATCH_COMPLETE ? $period : 0;
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
// Schedule as soon as possible if a batch is active.
'period' => $period,
'periodic' => TRUE,
);
if ($period != FEEDS_SCHEDULE_NEVER) {
JobScheduler::get('feeds_source_import')->set($job);
}
else {
JobScheduler::get('feeds_source_import')->remove($job);
}
}
/**
* Schedule background clearing tasks.
*/
public function scheduleClear() {
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
'period' => 0,
'periodic' => TRUE,
);
// Remove job if batch is complete.
if ($this->progressClearing() === FEEDS_BATCH_COMPLETE) {
JobScheduler::get('feeds_source_clear')->remove($job);
}
// Schedule as soon as possible if batch is not complete.
else {
JobScheduler::get('feeds_source_clear')->set($job);
}
}
/**
* Import a source: execute fetching, parsing and processing stage.
*
* This method only executes the current batch chunk, then returns. If you are
* looking to import an entire source, use FeedsSource::startImport() instead.
*
* @return
* FEEDS_BATCH_COMPLETE if the import process finished. A decimal between
* 0.0 and 0.9 periodic if import is still in progress.
*
* @throws
* Throws Exception if an error occurs when importing.
*/
public function import() {
$this->acquireLock();
try {
// If fetcher result is empty, we are starting a new import, log.
if (empty($this->fetcher_result)) {
$this->state[FEEDS_START] = time();
}
// Fetch.
if (empty($this->fetcher_result) || FEEDS_BATCH_COMPLETE == $this->progressParsing()) {
$this->fetcher_result = $this->importer->fetcher->fetch($this);
// Clean the parser's state, we are parsing an entirely new file.
unset($this->state[FEEDS_PARSE]);
}
// Parse.
$parser_result = $this->importer->parser->parse($this, $this->fetcher_result);
module_invoke_all('feeds_after_parse', $this, $parser_result);
// Process.
$this->importer->processor->process($this, $parser_result);
}
catch (Exception $e) {
// Do nothing.
}
$this->releaseLock();
// Clean up.
$result = $this->progressImporting();
if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
$this->imported = time();
$this->log('import', 'Imported in !s s', array('!s' => $this->imported - $this->state[FEEDS_START]), WATCHDOG_INFO);
module_invoke_all('feeds_after_import', $this);
unset($this->fetcher_result, $this->state);
}
$this->save();
if (isset($e)) {
throw $e;
}
return $result;
}
/**
* Remove all items from a feed.
*
* This method only executes the current batch chunk, then returns. If you are
* looking to delete all items of a source, use FeedsSource::startClear()
* instead.
*
* @return
* FEEDS_BATCH_COMPLETE if the clearing process finished. A decimal between
* 0.0 and 0.9 periodic if clearing is still in progress.
*
* @throws
* Throws Exception if an error occurs when clearing.
*/
public function clear() {
$this->acquireLock();
try {
$this->importer->fetcher->clear($this);
$this->importer->parser->clear($this);
$this->importer->processor->clear($this);
}
catch (Exception $e) {
// Do nothing.
}
$this->releaseLock();
// Clean up.
$result = $this->progressClearing();
if ($result == FEEDS_BATCH_COMPLETE || isset($e)) {
module_invoke_all('feeds_after_clear', $this);
unset($this->state);
}
$this->save();
if (isset($e)) {
throw $e;
}
return $result;
}
/**
* Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
*/
public function progressParsing() {
return $this->state(FEEDS_PARSE)->progress;
}
/**
* Report progress as float between 0 and 1. 1 = FEEDS_BATCH_COMPLETE.
*/
public function progressImporting() {
$fetcher = $this->state(FEEDS_FETCH);
$parser = $this->state(FEEDS_PARSE);
if ($fetcher->progress == FEEDS_BATCH_COMPLETE && $parser->progress == FEEDS_BATCH_COMPLETE) {
return FEEDS_BATCH_COMPLETE;
}
// Fetching envelops parsing.
// @todo: this assumes all fetchers neatly use total. May not be the case.
$fetcher_fraction = $fetcher->total ? 1.0 / $fetcher->total : 1.0;
$parser_progress = $parser->progress * $fetcher_fraction;
$result = $fetcher->progress - $fetcher_fraction + $parser_progress;
if ($result == FEEDS_BATCH_COMPLETE) {
return 0.99;
}
return $result;
}
/**
* Report progress on clearing.
*/
public function progressClearing() {
return $this->state(FEEDS_PROCESS_CLEAR)->progress;
}
/**
* Return a state object for a given stage. Lazy instantiates new states.
*
* @todo Rename getConfigFor() accordingly to config().
*
* @param $stage
* One of FEEDS_FETCH, FEEDS_PARSE, FEEDS_PROCESS or FEEDS_PROCESS_CLEAR.
*
* @return
* The FeedsState object for the given stage.
*/
public function state($stage) {
if (!is_array($this->state)) {
$this->state = array();
}
if (!isset($this->state[$stage])) {
$this->state[$stage] = new FeedsState();
}
return $this->state[$stage];
}
/**
* Count items imported by this source.
*/
public function itemCount() {
return $this->importer->processor->itemCount($this);
}
/**
* Save configuration.
*/
public function save() {
// Alert implementers of FeedsSourceInterface to the fact that we're saving.
foreach ($this->importer->plugin_types as $type) {
$this->importer->$type->sourceSave($this);
}
$config = $this->getConfig();
// Store the source property of the fetcher in a separate column so that we
// can do fast lookups on it.
$source = '';
if (isset($config[get_class($this->importer->fetcher)]['source'])) {
$source = $config[get_class($this->importer->fetcher)]['source'];
}
$object = array(
'id' => $this->id,
'feed_nid' => $this->feed_nid,
'imported' => $this->imported,
'config' => $config,
'source' => $source,
'state' => isset($this->state) ? $this->state : FALSE,
'fetcher_result' => isset($this->fetcher_result) ? $this->fetcher_result : FALSE,
);
if (db_query_range("SELECT 1 FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", 0, 1, array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchField()) {
drupal_write_record('feeds_source', $object, array('id', 'feed_nid'));
}
else {
drupal_write_record('feeds_source', $object);
}
}
/**
* Load configuration and unpack.
*
* @todo Patch CTools to move constants from export.inc to ctools.module.
*/
public function load() {
if ($record = db_query("SELECT imported, config, state, fetcher_result FROM {feeds_source} WHERE id = :id AND feed_nid = :nid", array(':id' => $this->id, ':nid' => $this->feed_nid))->fetchObject()) {
// While FeedsSource cannot be exported, we still use CTool's export.inc
// export definitions.
ctools_include('export');
$this->export_type = EXPORT_IN_DATABASE;
$this->imported = $record->imported;
$this->config = unserialize($record->config);
if (!empty($record->state)) {
$this->state = unserialize($record->state);
}
if (!empty($record->fetcher_result)) {
$this->fetcher_result = unserialize($record->fetcher_result);
}
}
}
/**
* Delete configuration. Removes configuration information
* from database, does not delete configuration itself.
*/
public function delete() {
// Alert implementers of FeedsSourceInterface to the fact that we're
// deleting.
foreach ($this->importer->plugin_types as $type) {
$this->importer->$type->sourceDelete($this);
}
db_delete('feeds_source')
->condition('id', $this->id)
->condition('feed_nid', $this->feed_nid)
->execute();
// Remove from schedule.
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
);
JobScheduler::get('feeds_source_import')->remove($job);
}
/**
* Only return source if configuration is persistent and valid.
*
* @see FeedsConfigurable::existing().
*/
public function existing() {
// If there is no feed nid given, there must be no content type specified.
// If there is a feed nid given, there must be a content type specified.
// Ensure that importer is persistent (= defined in code or DB).
// Ensure that source is persistent (= defined in DB).
if ((empty($this->feed_nid) && empty($this->importer->config['content_type'])) ||
(!empty($this->feed_nid) && !empty($this->importer->config['content_type']))) {
$this->importer->existing();
return parent::existing();
}
throw new FeedsNotExistingException(t('Source configuration not valid.'));
}
/**
* Returns the configuration for a specific client class.
*
* @param FeedsSourceInterface $client
* An object that is an implementer of FeedsSourceInterface.
*
* @return
* An array stored for $client.
*/
public function getConfigFor(FeedsSourceInterface $client) {
$class = get_class($client);
return isset($this->config[$class]) ? $this->config[$class] : $client->sourceDefaults();
}
/**
* Sets the configuration for a specific client class.
*
* @param FeedsSourceInterface $client
* An object that is an implementer of FeedsSourceInterface.
* @param $config
* The configuration for $client.
*
* @return
* An array stored for $client.
*/
public function setConfigFor(FeedsSourceInterface $client, $config) {
$this->config[get_class($client)] = $config;
}
/**
* Return defaults for feed configuration.
*/
public function configDefaults() {
// Collect information from plugins.
$defaults = array();
foreach ($this->importer->plugin_types as $type) {
if ($this->importer->$type->hasSourceConfig()) {
$defaults[get_class($this->importer->$type)] = $this->importer->$type->sourceDefaults();
}
}
return $defaults;
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
// Collect information from plugins.
$form = array();
foreach ($this->importer->plugin_types as $type) {
if ($this->importer->$type->hasSourceConfig()) {
$class = get_class($this->importer->$type);
$config = isset($this->config[$class]) ? $this->config[$class] : array();
$form[$class] = $this->importer->$type->sourceForm($config);
$form[$class]['#tree'] = TRUE;
}
}
return $form;
}
/**
* Override parent::configFormValidate().
*/
public function configFormValidate(&$values) {
foreach ($this->importer->plugin_types as $type) {
$class = get_class($this->importer->$type);
if (isset($values[$class]) && $this->importer->$type->hasSourceConfig()) {
$this->importer->$type->sourceFormValidate($values[$class]);
}
}
}
/**
* Writes to feeds log.
*/
public function log($type, $message, $variables = array(), $severity = WATCHDOG_NOTICE) {
feeds_log($this->id, $this->feed_nid, $type, $message, $variables, $severity);
}
/**
* Background job helper. Starts a background job using Job Scheduler.
*
* Execute the first batch chunk of a background job on the current page load,
* moves the rest of the job processing to a cron powered background job.
*
* Executing the first batch chunk is important, otherwise, when a user
* submits a source for import or clearing, we will leave her without any
* visual indicators of an ongoing job.
*
* @see FeedsSource::startImport().
* @see FeedsSource::startClear().
*
* @param $method
* Method to execute on importer; one of 'import' or 'clear'.
*
* @throws Exception $e
*/
protected function startBackgroundJob($method) {
if (FEEDS_BATCH_COMPLETE != $this->$method()) {
$job = array(
'type' => $this->id,
'id' => $this->feed_nid,
'period' => 0,
'periodic' => FALSE,
);
JobScheduler::get("feeds_source_{$method}")->set($job);
}
}
/**
* Batch API helper. Starts a Batch API job.
*
* @see FeedsSource::startImport().
* @see FeedsSource::startClear().
* @see feeds_batch()
*
* @param $title
* Title to show to user when executing batch.
* @param $method
* Method to execute on importer; one of 'import' or 'clear'.
*/
protected function startBatchAPIJob($title, $method) {
$batch = array(
'title' => $title,
'operations' => array(
array('feeds_batch', array($method, $this->id, $this->feed_nid)),
),
'progress_message' => '',
);
batch_set($batch);
}
/**
* Acquires a lock for this source.
*
* @throws FeedsLockException
* If a lock for the requested job could not be acquired.
*/
protected function acquireLock() {
if (!lock_acquire("feeds_source_{$this->id}_{$this->feed_nid}", 60.0)) {
throw new FeedsLockException(t('Cannot acquire lock for source @id / @feed_nid.', array('@id' => $this->id, '@feed_nid' => $this->feed_nid)));
}
}
/**
* Releases a lock for this source.
*/
protected function releaseLock() {
lock_release("feeds_source_{$this->id}_{$this->feed_nid}");
}
}

View File

@@ -0,0 +1,327 @@
<?php
/**
* @file
* Contains CSV Parser.
*
* Functions in this file are independent of the Feeds specific implementation.
* Thanks to jpetso http://drupal.org/user/56020 for most of the code in this
* file.
*/
/**
* Text lines from file iterator.
*/
class ParserCSVIterator implements Iterator {
private $handle;
private $currentLine;
private $currentPos;
public function __construct($filepath) {
$this->handle = fopen($filepath, 'r');
$this->currentLine = NULL;
$this->currentPos = NULL;
}
function __destruct() {
if ($this->handle) {
fclose($this->handle);
}
}
public function rewind($pos = 0) {
if ($this->handle) {
fseek($this->handle, $pos);
$this->next();
}
}
public function next() {
if ($this->handle) {
$this->currentLine = feof($this->handle) ? NULL : fgets($this->handle);
$this->currentPos = ftell($this->handle);
return $this->currentLine;
}
}
public function valid() {
return isset($this->currentLine);
}
public function current() {
return $this->currentLine;
}
public function currentPos() {
return $this->currentPos;
}
public function key() {
return 'line';
}
}
/**
* Functionality to parse CSV files into a two dimensional array.
*/
class ParserCSV {
private $delimiter;
private $skipFirstLine;
private $columnNames;
private $timeout;
private $timeoutReached;
private $startByte;
private $lineLimit;
private $lastLinePos;
public function __construct() {
$this->delimiter = ',';
$this->skipFirstLine = FALSE;
$this->columnNames = FALSE;
$this->timeout = FALSE;
$this->timeoutReached = FALSE;
$this->startByte = 0;
$this->lineLimit = 0;
$this->lastLinePos = 0;
}
/**
* Set the column delimiter string.
* By default, the comma (',') is used as delimiter.
*/
public function setDelimiter($delimiter) {
$this->delimiter = $delimiter;
}
/**
* Set this to TRUE if the parser should skip the first line of the CSV text,
* which might be desired if the first line contains the column names.
* By default, this is set to FALSE and the first line is not skipped.
*/
public function setSkipFirstLine($skipFirstLine) {
$this->skipFirstLine = $skipFirstLine;
}
/**
* Specify an array of column names if you know them in advance, or FALSE
* (which is the default) to unset any prior column names. If no column names
* are set, the parser will put each row into a simple numerically indexed
* array. If column names are given, the parser will create arrays with
* these column names as array keys instead.
*/
public function setColumnNames($columnNames) {
$this->columnNames = $columnNames;
}
/**
* Define the time (in milliseconds) after which the parser stops parsing,
* even if it has not yet finished processing the CSV data. If the timeout
* has been reached before parsing is done, the parse() method will return
* an incomplete list of rows - a single row will never be cut off in the
* middle, though. By default, no timeout (@p $timeout == FALSE) is defined.
*
* You can check if the timeout has been reached by calling the
* timeoutReached() method after parse() has been called.
*/
public function setTimeout($timeout) {
$this->timeout = $timeout;
}
/**
* After calling the parse() method, determine if the timeout (set by the
* setTimeout() method) has been reached.
*
* @deprecated Use lastLinePos() instead to determine whether a file has
* finished parsing.
*/
public function timeoutReached() {
return $this->timeoutReached;
}
/**
* Define the number of lines to parse in one parsing operation.
*
* By default, all lines of a file are being parsed.
*/
public function setLineLimit($lines) {
$this->lineLimit = $lines;
}
/**
* Get the byte number where the parser left off after last parse() call.
*
* @return
* 0 if all lines or no line has been parsed, the byte position of where a
* timeout or the line limit has been reached otherwise. This position can be
* used to set the start byte for the next iteration after parse() has
* reached the timeout set with setTimeout() or the line limit set with
* setLineLimit().
*
* @see ParserCSV::setStartByte()
*/
public function lastLinePos() {
return $this->lastLinePos;
}
/**
* Set the byte where file should be started to read.
*
* Useful when parsing a file in batches.
*/
public function setStartByte($start) {
return $this->startByte = $start;
}
/**
* Parse CSV files into a two dimensional array.
*
* @param Iterator $lineIterator
* An Iterator object that yields line strings, e.g. ParserCSVIterator.
* @param $start
* The byte number from where to start parsing the file.
* @param $lines
* The number of lines to parse, 0 for all lines.
* @return
* Two dimensional array that contains the data in the CSV file.
*/
public function parse(Iterator $lineIterator) {
$skipLine = $this->skipFirstLine;
$rows = array();
$this->timeoutReached = FALSE;
$this->lastLinePos = 0;
$maxTime = empty($this->timeout) ? FALSE : (microtime() + $this->timeout);
$linesParsed = 0;
for ($lineIterator->rewind($this->startByte); $lineIterator->valid(); $lineIterator->next()) {
// Make really sure we've got lines without trailing newlines.
$line = trim($lineIterator->current(), "\r\n");
// Skip empty lines.
if (empty($line)) {
continue;
}
// If the first line contains column names, skip it.
if ($skipLine) {
$skipLine = FALSE;
continue;
}
// The actual parser. explode() is unfortunately not suitable because the
// delimiter might be located inside a quoted field, and that would break
// the field and/or require additional effort to re-join the fields.
$quoted = FALSE;
$currentIndex = 0;
$currentField = '';
$fields = array();
// We must use strlen() as we're parsing byte by byte using strpos(), so
// drupal_strlen() will not work properly.
while ($currentIndex <= strlen($line)) {
if ($quoted) {
$nextQuoteIndex = strpos($line, '"', $currentIndex);
if ($nextQuoteIndex === FALSE) {
// There's a line break before the quote is closed, so fetch the
// next line and start from there.
$currentField .= substr($line, $currentIndex);
$lineIterator->next();
if (!$lineIterator->valid()) {
// Whoa, an unclosed quote! Well whatever, let's just ignore
// that shortcoming and record it nevertheless.
$fields[] = $currentField;
break;
}
// Ok, so, on with fetching the next line, as mentioned above.
$currentField .= "\n";
$line = trim($lineIterator->current(), "\r\n");
$currentIndex = 0;
continue;
}
// There's actually another quote in this line...
// find out whether it's escaped or not.
$currentField .= substr($line, $currentIndex, $nextQuoteIndex - $currentIndex);
if (isset($line[$nextQuoteIndex + 1]) && $line[$nextQuoteIndex + 1] === '"') {
// Escaped quote, add a single one to the field and proceed quoted.
$currentField .= '"';
$currentIndex = $nextQuoteIndex + 2;
}
else {
// End of the quoted section, close the quote and let the
// $quoted == FALSE block finalize the field.
$quoted = FALSE;
$currentIndex = $nextQuoteIndex + 1;
}
}
else { // $quoted == FALSE
// First, let's find out where the next character of interest is.
$nextQuoteIndex = strpos($line, '"', $currentIndex);
$nextDelimiterIndex = strpos($line, $this->delimiter, $currentIndex);
if ($nextQuoteIndex === FALSE) {
$nextIndex = $nextDelimiterIndex;
}
elseif ($nextDelimiterIndex === FALSE) {
$nextIndex = $nextQuoteIndex;
}
else {
$nextIndex = min($nextQuoteIndex, $nextDelimiterIndex);
}
if ($nextIndex === FALSE) {
// This line is done, add the rest of it as last field.
$currentField .= substr($line, $currentIndex);
$fields[] = $currentField;
break;
}
elseif ($line[$nextIndex] === $this->delimiter[0]) {
$length = ($nextIndex + strlen($this->delimiter) - 1) - $currentIndex;
$currentField .= substr($line, $currentIndex, $length);
$fields[] = $currentField;
$currentField = '';
$currentIndex += $length + 1;
// Continue with the next field.
}
else { // $line[$nextIndex] == '"'
$quoted = TRUE;
$currentField .= substr($line, $currentIndex, $nextIndex - $currentIndex);
$currentIndex = $nextIndex + 1;
// Continue this field in the $quoted == TRUE block.
}
}
}
// End of CSV parser. We've now got all the fields of the line as strings
// in the $fields array.
if (empty($this->columnNames)) {
$row = $fields;
}
else {
$row = array();
foreach ($this->columnNames as $columnName) {
$field = array_shift($fields);
$row[$columnName] = isset($field) ? $field : '';
}
}
$rows[] = $row;
// Quit parsing if timeout has been reached or requested lines have been
// reached.
if (!empty($maxTime) && microtime() > $maxTime) {
$this->timeoutReached = TRUE;
$this->lastLinePos = $lineIterator->currentPos();
break;
}
$linesParsed++;
if ($this->lineLimit && $linesParsed >= $this->lineLimit) {
$this->lastLinePos = $lineIterator->currentPos();
break;
}
}
return $rows;
}
}

View File

@@ -0,0 +1,390 @@
<?php
/**
* @file
* Pubsubhubbub subscriber library.
*
* Readme
* http://github.com/lxbarth/PuSHSubscriber
*
* License
* http://github.com/lxbarth/PuSHSubscriber/blob/master/LICENSE.txt
*/
/**
* PubSubHubbub subscriber.
*/
class PuSHSubscriber {
protected $domain;
protected $subscriber_id;
protected $subscription_class;
protected $env;
/**
* Singleton.
*
* PuSHSubscriber identifies a unique subscription by a domain and a numeric
* id. The numeric id is assumed to e unique in its domain.
*
* @param $domain
* A string that identifies the domain in which $subscriber_id is unique.
* @param $subscriber_id
* A numeric subscriber id.
* @param $subscription_class
* The class to use for handling subscriptions. Class MUST implement
* PuSHSubscriberSubscriptionInterface
* @param PuSHSubscriberEnvironmentInterface $env
* Environmental object for messaging and logging.
*/
public static function instance($domain, $subscriber_id, $subscription_class, PuSHSubscriberEnvironmentInterface $env) {
static $subscribers;
if (!isset($subscriber[$domain][$subscriber_id])) {
$subscriber = new PuSHSubscriber($domain, $subscriber_id, $subscription_class, $env);
}
return $subscriber;
}
/**
* Protect constructor.
*/
protected function __construct($domain, $subscriber_id, $subscription_class, PuSHSubscriberEnvironmentInterface $env) {
$this->domain = $domain;
$this->subscriber_id = $subscriber_id;
$this->subscription_class = $subscription_class;
$this->env = $env;
}
/**
* Subscribe to a given URL. Attempt to retrieve 'hub' and 'self' links from
* document at $url and issue a subscription request to the hub.
*
* @param $url
* The URL of the feed to subscribe to.
* @param $callback_url
* The full URL that hub should invoke for subscription verification or for
* notifications.
* @param $hub
* The URL of a hub. If given overrides the hub URL found in the document
* at $url.
*/
public function subscribe($url, $callback_url, $hub = '') {
// Fetch document, find rel=hub and rel=self.
// If present, issue subscription request.
$request = curl_init($url);
curl_setopt($request, CURLOPT_FOLLOWLOCATION, TRUE);
curl_setopt($request, CURLOPT_RETURNTRANSFER, TRUE);
$data = curl_exec($request);
if (curl_getinfo($request, CURLINFO_HTTP_CODE) == 200) {
try {
$xml = @ new SimpleXMLElement($data);
$xml->registerXPathNamespace('atom', 'http://www.w3.org/2005/Atom');
if (empty($hub) && $hub = @current($xml->xpath("//atom:link[attribute::rel='hub']"))) {
$hub = (string) $hub->attributes()->href;
}
if ($self = @current($xml->xpath("//atom:link[attribute::rel='self']"))) {
$self = (string) $self->attributes()->href;
}
}
catch (Exception $e) {
// Do nothing.
}
}
curl_close($request);
// Fall back to $url if $self is not given.
if (!$self) {
$self = $url;
}
if (!empty($hub) && !empty($self)) {
$this->request($hub, $self, 'subscribe', $callback_url);
}
}
/**
* @todo Unsubscribe from a hub.
* @todo Make sure we unsubscribe with the correct topic URL as it can differ
* from the initial subscription URL.
*
* @param $topic_url
* The URL of the topic to unsubscribe from.
* @param $callback_url
* The callback to unsubscribe.
*/
public function unsubscribe($topic_url, $callback_url) {
if ($sub = $this->subscription()) {
$this->request($sub->hub, $sub->topic, 'unsubscribe', $callback_url);
$sub->delete();
}
}
/**
* Request handler for subscription callbacks.
*/
public function handleRequest($callback) {
if (isset($_GET['hub_challenge'])) {
$this->verifyRequest();
}
// No subscription notification has ben sent, we are being notified.
else {
if ($raw = $this->receive()) {
$callback($raw, $this->domain, $this->subscriber_id);
}
}
}
/**
* Receive a notification.
*
* @param $ignore_signature
* If FALSE, only accept payload if there is a signature present and the
* signature matches the payload. Warning: setting to TRUE results in
* unsafe behavior.
*
* @return
* An XML string that is the payload of the notification if valid, FALSE
* otherwise.
*/
public function receive($ignore_signature = FALSE) {
/**
* Verification steps:
*
* 1) Verify that this is indeed a POST reuest.
* 2) Verify that posted string is XML.
* 3) Per default verify sender of message by checking the message's
* signature against the shared secret.
*/
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$raw = file_get_contents('php://input');
if (@simplexml_load_string($raw)) {
if ($ignore_signature) {
return $raw;
}
if (isset($_SERVER['HTTP_X_HUB_SIGNATURE']) && ($sub = $this->subscription())) {
$result = array();
parse_str($_SERVER['HTTP_X_HUB_SIGNATURE'], $result);
if (isset($result['sha1']) && $result['sha1'] == hash_hmac('sha1', $raw, $sub->secret)) {
return $raw;
}
else {
$this->log('Could not verify signature.', 'error');
}
}
else {
$this->log('No signature present.', 'error');
}
}
}
return FALSE;
}
/**
* Verify a request. After a hub has received a subscribe or unsubscribe
* request (see PuSHSubscriber::request()) it sends back a challenge verifying
* that an action indeed was requested ($_GET['hub_challenge']). This
* method handles the challenge.
*/
public function verifyRequest() {
if (isset($_GET['hub_challenge'])) {
/**
* If a subscription is present, compare the verify token. If the token
* matches, set the status on the subscription record and confirm
* positive.
*
* If we cannot find a matching subscription and the hub checks on
* 'unsubscribe' confirm positive.
*
* In all other cases confirm negative.
*/
if ($sub = $this->subscription()) {
if ($_GET['hub_verify_token'] == $sub->post_fields['hub.verify_token']) {
if ($_GET['hub_mode'] == 'subscribe' && $sub->status == 'subscribe') {
$sub->status = 'subscribed';
$sub->post_fields = array();
$sub->save();
$this->log('Verified "subscribe" request.');
$verify = TRUE;
}
elseif ($_GET['hub_mode'] == 'unsubscribe' && $sub->status == 'unsubscribe') {
$sub->status = 'unsubscribed';
$sub->post_fields = array();
$sub->save();
$this->log('Verified "unsubscribe" request.');
$verify = TRUE;
}
}
}
elseif ($_GET['hub_mode'] == 'unsubscribe') {
$this->log('Verified "unsubscribe" request.');
$verify = TRUE;
}
if ($verify) {
header('HTTP/1.1 200 "Found"', NULL, 200);
print $_GET['hub_challenge'];
drupal_exit();
}
}
header('HTTP/1.1 404 "Not Found"', NULL, 404);
$this->log('Could not verify subscription.', 'error');
drupal_exit();
}
/**
* Issue a subscribe or unsubcribe request to a PubsubHubbub hub.
*
* @param $hub
* The URL of the hub's subscription endpoint.
* @param $topic
* The topic URL of the feed to subscribe to.
* @param $mode
* 'subscribe' or 'unsubscribe'.
* @param $callback_url
* The subscriber's notifications callback URL.
*
* Compare to http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.2.html#anchor5
*
* @todo Make concurrency safe.
*/
protected function request($hub, $topic, $mode, $callback_url) {
$secret = hash('sha1', uniqid(rand(), TRUE));
$post_fields = array(
'hub.callback' => $callback_url,
'hub.mode' => $mode,
'hub.topic' => $topic,
'hub.verify' => 'sync',
'hub.lease_seconds' => '', // Permanent subscription.
'hub.secret' => $secret,
'hub.verify_token' => md5(session_id() . rand()),
);
$sub = new $this->subscription_class($this->domain, $this->subscriber_id, $hub, $topic, $secret, $mode, $post_fields);
$sub->save();
// Issue subscription request.
$request = curl_init($hub);
curl_setopt($request, CURLOPT_POST, TRUE);
curl_setopt($request, CURLOPT_POSTFIELDS, $post_fields);
curl_setopt($request, CURLOPT_RETURNTRANSFER, TRUE);
curl_exec($request);
$code = curl_getinfo($request, CURLINFO_HTTP_CODE);
if (in_array($code, array(202, 204))) {
$this->log("Positive response to \"$mode\" request ($code).");
}
else {
$sub->status = $mode . ' failed';
$sub->save();
$this->log("Error issuing \"$mode\" request to $hub ($code).", 'error');
}
curl_close($request);
}
/**
* Get the subscription associated with this subscriber.
*
* @return
* A PuSHSubscriptionInterface object if a subscription exist, NULL
* otherwise.
*/
public function subscription() {
return call_user_func(array($this->subscription_class, 'load'), $this->domain, $this->subscriber_id);
}
/**
* Determine whether this subscriber is successfully subscribed or not.
*/
public function subscribed() {
if ($sub = $this->subscription()) {
if ($sub->status == 'subscribed') {
return TRUE;
}
}
return FALSE;
}
/**
* Helper for messaging.
*/
protected function msg($msg, $level = 'status') {
$this->env->msg($msg, $level);
}
/**
* Helper for logging.
*/
protected function log($msg, $level = 'status') {
$this->env->log("{$this->domain}:{$this->subscriber_id}\t$msg", $level);
}
}
/**
* Implement to provide a storage backend for subscriptions.
*
* Variables passed in to the constructor must be accessible as public class
* variables.
*/
interface PuSHSubscriptionInterface {
/**
* @param $domain
* A string that defines the domain in which the subscriber_id is unique.
* @param $subscriber_id
* A unique numeric subscriber id.
* @param $hub
* The URL of the hub endpoint.
* @param $topic
* The topic to subscribe to.
* @param $secret
* A secret key used for message authentication.
* @param $status
* The status of the subscription.
* 'subscribe' - subscribing to a feed.
* 'unsubscribe' - unsubscribing from a feed.
* 'subscribed' - subscribed.
* 'unsubscribed' - unsubscribed.
* 'subscribe failed' - subscribe request failed.
* 'unsubscribe failed' - unsubscribe request failed.
* @param $post_fields
* An array of the fields posted to the hub.
*/
public function __construct($domain, $subscriber_id, $hub, $topic, $secret, $status = '', $post_fields = '');
/**
* Save a subscription.
*/
public function save();
/**
* Load a subscription.
*
* @return
* A PuSHSubscriptionInterface object if a subscription exist, NULL
* otherwise.
*/
public static function load($domain, $subscriber_id);
/**
* Delete a subscription.
*/
public function delete();
}
/**
* Implement to provide environmental functionality like user messages and
* logging.
*/
interface PuSHSubscriberEnvironmentInterface {
/**
* A message to be displayed to the user on the current page load.
*
* @param $msg
* A string that is the message to be displayed.
* @param $level
* A string that is either 'status', 'warning' or 'error'.
*/
public function msg($msg, $level = 'status');
/**
* A log message to be logged to the database or the file system.
*
* @param $msg
* A string that is the message to be displayed.
* @param $level
* A string that is either 'status', 'warning' or 'error'.
*/
public function log($msg, $level = 'status');
}

View File

@@ -0,0 +1,590 @@
<?php
/**
* @file
* Downloading and parsing functions for Common Syndication Parser.
* Pillaged from FeedAPI common syndication parser.
*
* @todo Restructure. OO could work wonders here.
* @todo Write unit tests.
* @todo Keep in Feeds project or host on Drupal?
*/
/**
* Parse the feed into a data structure.
*
* @param $feed
* The feed object (contains the URL or the parsed XML structure.
* @return
* stdClass The structured datas extracted from the feed.
*/
function common_syndication_parser_parse($string) {
if (!defined('LIBXML_VERSION') || (version_compare(phpversion(), '5.1.0', '<'))) {
@ $xml = simplexml_load_string($string, NULL);
}
else {
@ $xml = simplexml_load_string($string, NULL, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NOCDATA);
}
// Got a malformed XML.
if ($xml === FALSE || is_null($xml)) {
return FALSE;
}
$feed_type = _parser_common_syndication_feed_format_detect($xml);
if ($feed_type == "atom1.0") {
return _parser_common_syndication_atom10_parse($xml);
}
if ($feed_type == "RSS2.0" || $feed_type == "RSS0.91" || $feed_type == "RSS0.92") {
return _parser_common_syndication_RSS20_parse($xml);
}
if ($feed_type == "RDF") {
return _parser_common_syndication_RDF10_parse($xml);
}
return FALSE;
}
/**
* Get the cached version of the <var>$url</var>
*/
function _parser_common_syndication_cache_get($url) {
$cache_file = _parser_common_syndication_sanitize_cache() . '/' . md5($url);
if (file_exists($cache_file)) {
$file_content = file_get_contents($cache_file);
return unserialize($file_content);
}
return FALSE;
}
/**
* Determine the feed format of a SimpleXML parsed object structure.
*
* @param $xml
* SimpleXML-preprocessed feed.
* @return
* The feed format short description or FALSE if not compatible.
*/
function _parser_common_syndication_feed_format_detect($xml) {
if (!is_object($xml)) {
return FALSE;
}
$attr = $xml->attributes();
$type = strtolower($xml->getName());
if (isset($xml->entry) && $type == "feed") {
return "atom1.0";
}
if ($type == "rss" && $attr["version"] == "2.0") {
return "RSS2.0";
}
if ($type == "rdf" && isset($xml->channel)) {
return "RDF";
}
if ($type == "rss" && $attr["version"] == "0.91") {
return "RSS0.91";
}
if ($type == "rss" && $attr["version"] == "0.92") {
return "RSS0.92";
}
return FALSE;
}
/**
* Parse atom feeds.
*/
function _parser_common_syndication_atom10_parse($feed_XML) {
$parsed_source = array();
$ns = array(
"georss" => "http://www.georss.org/georss",
);
$base = $feed_XML->xpath("@base");
$base = (string) array_shift($base);
if (!valid_url($base, TRUE)) {
$base = FALSE;
}
// Detect the title
$parsed_source['title'] = isset($feed_XML->title) ? _parser_common_syndication_title("{$feed_XML->title}") : "";
// Detect the description
$parsed_source['description'] = isset($feed_XML->subtitle) ? "{$feed_XML->subtitle}" : "";
$parsed_source['link'] = _parser_common_syndication_link($feed_XML->link);
if (valid_url($parsed_source['link']) && !valid_url($parsed_source['link'], TRUE) && !empty($base)) {
$parsed_source['link'] = $base . $parsed_source['link'];
}
$parsed_source['items'] = array();
foreach ($feed_XML->entry as $news) {
$original_url = NULL;
$guid = !empty($news->id) ? "{$news->id}" : NULL;
if (valid_url($guid, TRUE)) {
$original_url = $guid;
}
$georss = (array)$news->children($ns["georss"]);
$geoname = '';
if (isset($georss['featureName'])) {
$geoname = "{$georss['featureName']}";
}
$latlon =
$lat =
$lon = NULL;
if (isset($georss['point'])) {
$latlon = explode(' ', $georss['point']);
$lat = "{$latlon[0]}";
$lon = "{$latlon[1]}";
if (!$geoname) {
$geoname = "{$lat} {$lon}";
}
}
$additional_taxonomies = array();
if (isset($news->category)) {
$additional_taxonomies['ATOM Categories'] = array();
$additional_taxonomies['ATOM Domains'] = array();
foreach ($news->category as $category) {
if (isset($category['scheme'])) {
$domain = "{$category['scheme']}";
if (!empty($domain)) {
if (!isset($additional_taxonomies['ATOM Domains'][$domain])) {
$additional_taxonomies['ATOM Domains'][$domain] = array();
}
$additional_taxonomies['ATOM Domains'][$domain][] = count($additional_taxonomies['ATOM Categories']) - 1;
}
}
$additional_taxonomies['ATOM Categories'][] = "{$category['term']}";
}
}
$title = "{$news->title}";
$body = '';
if (!empty($news->content)) {
foreach ($news->content->children() as $child) {
$body .= $child->asXML();
}
$body .= "{$news->content}";
}
elseif (!empty($news->summary)) {
foreach ($news->summary->children() as $child) {
$body .= $child->asXML();
}
$body .= "{$news->summary}";
}
if (!empty($news->content['src'])) {
// some src elements in some valid atom feeds contained no urls at all
if (valid_url("{$news->content['src']}", TRUE)) {
$original_url = "{$news->content['src']}";
}
}
$author_found = FALSE;
if (!empty($news->source->author->name)) {
$original_author = "{$news->source->author->name}";
$author_found = TRUE;
}
elseif (!empty($news->author->name)) {
$original_author = "{$news->author->name}";
$author_found = TRUE;
}
if (!empty($feed_XML->author->name) && !$author_found) {
$original_author = "{$feed_XML->author->name}";
}
$original_url = _parser_common_syndication_link($news->link);
$item = array();
$item['title'] = _parser_common_syndication_title($title, $body);
$item['description'] = $body;
$item['author_name'] = $original_author;
// Fall back to updated for timestamp if both published and issued are
// empty.
if (isset($news->published)) {
$item['timestamp'] = _parser_common_syndication_parse_date("{$news->published}");
}
elseif (isset($news->issued)) {
$item['timestamp'] = _parser_common_syndication_parse_date("{$news->issued}");
}
elseif (isset($news->updated)) {
$item['timestamp'] = _parser_common_syndication_parse_date("{$news->updated}");
}
$item['url'] = trim($original_url);
if (valid_url($item['url']) && !valid_url($item['url'], TRUE) && !empty($base)) {
$item['url'] = $base . $item['url'];
}
// Fall back on URL if GUID is empty.
if (!empty($guid)) {
$item['guid'] = $guid;
}
else {
$item['guid'] = $item['url'];
}
$item['geolocations'] = array();
if ($lat && $lon) {
$item['geolocations'] = array(
array(
'name' => $geoname,
'lat' => $lat,
'lon' => $lon,
),
);
}
$item['tags'] = isset($additional_taxonomies['ATOM Categories']) ? $additional_taxonomies['ATOM Categories'] : array();
$item['domains'] = isset($additional_taxonomies['ATOM Domains']) ? $additional_taxonomies['ATOM Domains'] : array();
$parsed_source['items'][] = $item;
}
return $parsed_source;
}
/**
* Parse RDF Site Summary (RSS) 1.0 feeds in RDF/XML format.
*
* @see http://web.resource.org/rss/1.0/
*/
function _parser_common_syndication_RDF10_parse($feed_XML) {
// Declare some canonical standard prefixes for well-known namespaces:
static $canonical_namespaces = array(
'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#',
'xsi' => 'http://www.w3.org/2001/XMLSchema-instance#',
'xsd' => 'http://www.w3.org/2001/XMLSchema#',
'owl' => 'http://www.w3.org/2002/07/owl#',
'dc' => 'http://purl.org/dc/elements/1.1/',
'dcterms' => 'http://purl.org/dc/terms/',
'dcmitype' => 'http://purl.org/dc/dcmitype/',
'foaf' => 'http://xmlns.com/foaf/0.1/',
'rss' => 'http://purl.org/rss/1.0/',
);
// Get all namespaces declared in the feed element, with special handling
// for PHP versions prior to 5.1.2 as they don't handle namespaces.
$namespaces = version_compare(phpversion(), '5.1.2', '<') ? array() : $feed_XML->getNamespaces(TRUE);
// Process the <rss:channel> resource containing feed metadata:
foreach ($feed_XML->children($canonical_namespaces['rss'])->channel as $rss_channel) {
$parsed_source = array(
'title' => _parser_common_syndication_title((string) $rss_channel->title),
'description' => (string) $rss_channel->description,
'link' => (string) $rss_channel->link,
'items' => array(),
);
break;
}
// Process each <rss:item> resource contained in the feed:
foreach ($feed_XML->children($canonical_namespaces['rss'])->item as $rss_item) {
// Extract all available RDF statements from the feed item's RDF/XML
// tags, allowing for both the item's attributes and child elements to
// contain RDF properties:
$rdf_data = array();
foreach ($namespaces as $ns => $ns_uri) {
// Note that we attempt to normalize the found property name
// namespaces to well-known 'standard' prefixes where possible, as the
// feed may in principle use any arbitrary prefixes and we should
// still be able to correctly handle it.
foreach ($rss_item->attributes($ns_uri) as $attr_name => $attr_value) {
$ns_prefix = ($ns_prefix = array_search($ns_uri, $canonical_namespaces)) ? $ns_prefix : $ns;
$rdf_data[$ns_prefix . ':' . $attr_name][] = (string) $attr_value;
}
foreach ($rss_item->children($ns_uri) as $rss_property) {
$ns_prefix = ($ns_prefix = array_search($ns_uri, $canonical_namespaces)) ? $ns_prefix : $ns;
$rdf_data[$ns_prefix . ':' . $rss_property->getName()][] = (string) $rss_property;
}
}
// Declaratively define mappings that determine how to construct the result object.
$item = _parser_common_syndication_RDF10_item($rdf_data, array(
'title' => array('rss:title', 'dc:title'),
'description' => array('rss:description', 'dc:description', 'content:encoded'),
'url' => array('rss:link', 'rdf:about'),
'author_name' => array('dc:creator', 'dc:publisher'),
'guid' => 'rdf:about',
'timestamp' => 'dc:date',
'tags' => 'dc:subject'
));
// Special handling for the title:
$item['title'] = _parser_common_syndication_title($item['title'], $item['description']);
// Parse any date/time values into Unix timestamps:
$item['timestamp'] = _parser_common_syndication_parse_date($item['timestamp']);
// If no GUID found, use the URL of the feed.
if (empty($item['guid'])) {
$item['guid'] = $item['url'];
}
// Add every found RDF property to the feed item.
$item['rdf'] = array();
foreach ($rdf_data as $rdf_property => $rdf_value) {
// looks nicer in the mapper UI
// @todo Revisit, not used with feedapi mapper anymore.
$rdf_property = str_replace(':', '_', $rdf_property);
$item['rdf'][$rdf_property] = $rdf_value;
}
$parsed_source['items'][] = $item;
}
return $parsed_source;
}
function _parser_common_syndication_RDF10_property($rdf_data, $rdf_properties = array()) {
$rdf_properties = is_array($rdf_properties) ? $rdf_properties : array_slice(func_get_args(), 1);
foreach ($rdf_properties as $rdf_property) {
if ($rdf_property && !empty($rdf_data[$rdf_property])) {
// remove empty strings
return array_filter($rdf_data[$rdf_property], 'strlen');
}
}
}
function _parser_common_syndication_RDF10_item($rdf_data, $mappings) {
foreach ($mappings as $k => $v) {
$values = _parser_common_syndication_RDF10_property($rdf_data, $v);
$mappings[$k] = !is_array($values) || count($values) > 1 ? $values : reset($values);
}
return $mappings;
}
/**
* Parse RSS2.0 feeds.
*/
function _parser_common_syndication_RSS20_parse($feed_XML) {
$ns = array(
"content" => "http://purl.org/rss/1.0/modules/content/",
"dc" => "http://purl.org/dc/elements/1.1/",
"georss" => "http://www.georss.org/georss",
);
$parsed_source = array();
// Detect the title.
$parsed_source['title'] = isset($feed_XML->channel->title) ? _parser_common_syndication_title("{$feed_XML->channel->title}") : "";
// Detect the description.
$parsed_source['description'] = isset($feed_XML->channel->description) ? "{$feed_XML->channel->description}" : "";
// Detect the link.
$parsed_source['link'] = isset($feed_XML->channel->link) ? "{$feed_XML->channel->link}" : "";
$parsed_source['items'] = array();
foreach ($feed_XML->xpath('//item') as $news) {
$title = $body = $original_author = $original_url = $guid = '';
$category = $news->xpath('category');
// Get children for current namespace.
if (version_compare(phpversion(), '5.1.2', '>')) {
$content = (array)$news->children($ns["content"]);
$dc = (array)$news->children($ns["dc"]);
$georss = (array)$news->children($ns["georss"]);
}
$news = (array) $news;
$news['category'] = $category;
if (isset($news['title'])) {
$title = "{$news['title']}";
}
if (isset($news['description'])) {
$body = "{$news['description']}";
}
// Some sources use content:encoded as description i.e.
// PostNuke PageSetter module.
if (isset($news['encoded'])) { // content:encoded for PHP < 5.1.2.
if (strlen($body) < strlen("{$news['encoded']}")) {
$body = "{$news['encoded']}";
}
}
if (isset($content['encoded'])) { // content:encoded for PHP >= 5.1.2.
if (strlen($body) < strlen("{$content['encoded']}")) {
$body = "{$content['encoded']}";
}
}
if (!isset($body)) {
$body = "{$news['title']}";
}
if (!empty($news['author'])) {
$original_author = "{$news['author']}";
}
elseif (!empty($dc["creator"])) {
$original_author = (string)$dc["creator"];
}
if (!empty($news['link'])) {
$original_url = "{$news['link']}";
$guid = $original_url;
}
if (!empty($news['guid'])) {
$guid = "{$news['guid']}";
}
if (!empty($georss['featureName'])) {
$geoname = "{$georss['featureName']}";
}
$lat =
$lon =
$latlon =
$geoname = NULL;
if (!empty($georss['point'])) {
$latlon = explode(' ', $georss['point']);
$lat = "{$latlon[0]}";
$lon = "{$latlon[1]}";
if (!$geoname) {
$geoname = "$lat $lon";
}
}
$additional_taxonomies = array();
$additional_taxonomies['RSS Categories'] = array();
$additional_taxonomies['RSS Domains'] = array();
if (isset($news['category'])) {
foreach ($news['category'] as $category) {
$additional_taxonomies['RSS Categories'][] = "{$category}";
if (isset($category['domain'])) {
$domain = "{$category['domain']}";
if (!empty($domain)) {
if (!isset($additional_taxonomies['RSS Domains'][$domain])) {
$additional_taxonomies['RSS Domains'][$domain] = array();
}
$additional_taxonomies['RSS Domains'][$domain][] = count($additional_taxonomies['RSS Categories']) - 1;
}
}
}
}
$item = array();
$item['title'] = _parser_common_syndication_title($title, $body);
$item['description'] = $body;
$item['author_name'] = $original_author;
if (!empty($news['pubDate'])) {
$item['timestamp'] = _parser_common_syndication_parse_date($news['pubDate']);
}
elseif (!empty($dc['date'])) {
$item['timestamp'] = _parser_common_syndication_parse_date($dc['date']);
}
else {
$item['timestamp'] = time();
}
$item['url'] = trim($original_url);
$item['guid'] = $guid;
$item['geolocations'] = array();
if (isset($geoname, $lat, $lon)) {
$item['geolocations'] = array(
array(
'name' => $geoname,
'lat' => $lat,
'lon' => $lon,
),
);
}
$item['domains'] = $additional_taxonomies['RSS Domains'];
$item['tags'] = $additional_taxonomies['RSS Categories'];
$parsed_source['items'][] = $item;
}
return $parsed_source;
}
/**
* Parse a date comes from a feed.
*
* @param $date_string
* The date string in various formats.
* @return
* The timestamp of the string or the current time if can't be parsed
*/
function _parser_common_syndication_parse_date($date_str) {
// PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
$date_str = str_replace("GMT-", "-", $date_str);
$date_str = str_replace("GMT+", "+", $date_str);
$parsed_date = strtotime($date_str);
if ($parsed_date === FALSE || $parsed_date == -1) {
$parsed_date = _parser_common_syndication_parse_w3cdtf($date_str);
}
return $parsed_date === FALSE ? time() : $parsed_date;
}
/**
* Parse the W3C date/time format, a subset of ISO 8601.
*
* PHP date parsing functions do not handle this format.
* See http://www.w3.org/TR/NOTE-datetime for more information.
* Originally from MagpieRSS (http://magpierss.sourceforge.net/).
*
* @param $date_str
* A string with a potentially W3C DTF date.
* @return
* A timestamp if parsed successfully or FALSE if not.
*/
function _parser_common_syndication_parse_w3cdtf($date_str) {
if (preg_match('/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/', $date_str, $match)) {
list($year, $month, $day, $hours, $minutes, $seconds) = array($match[1], $match[2], $match[3], $match[4], $match[5], $match[6]);
// Calculate the epoch for current date assuming GMT.
$epoch = gmmktime($hours, $minutes, $seconds, $month, $day, $year);
if ($match[10] != 'Z') { // Z is zulu time, aka GMT
list($tz_mod, $tz_hour, $tz_min) = array($match[8], $match[9], $match[10]);
// Zero out the variables.
if (!$tz_hour) {
$tz_hour = 0;
}
if (!$tz_min) {
$tz_min = 0;
}
$offset_secs = (($tz_hour * 60) + $tz_min) * 60;
// Is timezone ahead of GMT? If yes, subtract offset.
if ($tz_mod == '+') {
$offset_secs *= -1;
}
$epoch += $offset_secs;
}
return $epoch;
}
else {
return FALSE;
}
}
/**
* Extract the link that points to the original content (back to site or
* original article)
*
* @param $links
* Array of SimpleXML objects
*/
function _parser_common_syndication_link($links) {
$to_link = '';
if (count($links) > 0) {
foreach ($links as $link) {
$link = $link->attributes();
$to_link = isset($link["href"]) ? "{$link["href"]}" : "";
if (isset($link["rel"])) {
if ("{$link["rel"]}" == 'alternate') {
break;
}
}
}
}
return $to_link;
}
/**
* Prepare raw data to be a title
*/
function _parser_common_syndication_title($title, $body = FALSE) {
if (empty($title) && !empty($body)) {
// Explode to words and use the first 3 words.
$words = preg_split('/[\s,]+/', strip_tags($body));
$title = implode(' ', array_slice($words, 0, 3));
}
return $title;
}

View File

@@ -0,0 +1,405 @@
<?php
/**
* @file
* Download via HTTP.
*
* Support caching, HTTP Basic Authentication, detection of RSS/Atom feeds,
* redirects.
*/
/**
* PCRE for finding the link tags in html.
*/
define('HTTP_REQUEST_PCRE_LINK_TAG', '/<link((?:[\x09\x0A\x0B\x0C\x0D\x20]+[^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"(?:[^"]*)"|\'(?:[^\']*)\'|(?:[^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?)*)[\x09\x0A\x0B\x0C\x0D\x20]*(>(.*)<\/link>|(\/)?>)/si');
/**
* PCRE for matching all the attributes in a tag.
*/
define('HTTP_REQUEST_PCRE_TAG_ATTRIBUTES', '/[\x09\x0A\x0B\x0C\x0D\x20]+([^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*)(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"([^"]*)"|\'([^\']*)\'|([^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?/');
/**
* For cUrl specific errors.
*/
class HRCurlException extends Exception {}
/**
* Discover RSS or atom feeds at the given URL. If document in given URL is an
* HTML document, function attempts to discover RSS or Atom feeds.
*
* @param string $url
* The url of the feed to retrieve.
* @param array $settings
* An optional array of settings. Valid options are: accept_invalid_cert.
*
* @return bool|string
* The discovered feed, or FALSE if the URL is not reachable or there was an
* error.
*/
function http_request_get_common_syndication($url, $settings = NULL) {
$accept_invalid_cert = isset($settings['accept_invalid_cert']) ? $settings['accept_invalid_cert'] : FALSE;
$download = http_request_get($url, NULL, NULL, $accept_invalid_cert);
// Cannot get the feed, return.
// http_request_get() always returns 200 even if its 304.
if ($download->code != 200) {
return FALSE;
}
// Drop the data into a seperate variable so all manipulations of the html
// will not effect the actual object that exists in the static cache.
// @see http_request_get.
$downloaded_string = $download->data;
// If this happens to be a feed then just return the url.
if (http_request_is_feed($download->headers['content-type'], $downloaded_string)) {
return $url;
}
$discovered_feeds = http_request_find_feeds($downloaded_string);
foreach ($discovered_feeds as $feed_url) {
$absolute = http_request_create_absolute_url($feed_url, $url);
if (!empty($absolute)) {
// @TODO: something more intelligent?
return $absolute;
}
}
}
/**
* Get the content from the given URL.
*
* @param string $url
* A valid URL (not only web URLs).
* @param string $username
* If the URL uses authentication, supply the username.
* @param string $password
* If the URL uses authentication, supply the password.
* @param bool $accept_invalid_cert
* Whether to accept invalid certificates.
* @return stdClass
* An object that describes the data downloaded from $url.
*/
function http_request_get($url, $username = NULL, $password = NULL, $accept_invalid_cert = FALSE) {
// Intra-pagedownload cache, avoid to download the same content twice within
// one page download (it's possible, compatible and parse calls).
static $download_cache = array();
if (isset($download_cache[$url])) {
return $download_cache[$url];
}
if (!$username && valid_url($url, TRUE)) {
// Handle password protected feeds.
$url_parts = parse_url($url);
if (!empty($url_parts['user'])) {
$password = $url_parts['pass'];
$username = $url_parts['user'];
}
}
$curl = http_request_use_curl();
// Only download and parse data if really needs refresh.
// Based on "Last-Modified" and "If-Modified-Since".
$headers = array();
if ($cache = cache_get('feeds_http_download_' . md5($url))) {
$last_result = $cache->data;
$last_headers = array_change_key_case($last_result->headers);
if (!empty($last_headers['etag'])) {
if ($curl) {
$headers[] = 'If-None-Match: ' . $last_headers['etag'];
}
else {
$headers['If-None-Match'] = $last_headers['etag'];
}
}
if (!empty($last_headers['last-modified'])) {
if ($curl) {
$headers[] = 'If-Modified-Since: ' . $last_headers['last-modified'];
}
else {
$headers['If-Modified-Since'] = $last_headers['last-modified'];
}
}
if (!empty($username) && !$curl) {
$headers['Authorization'] = 'Basic ' . base64_encode("$username:$password");
}
}
// Support the 'feed' and 'webcal' schemes by converting them into 'http'.
$url = strtr($url, array('feed://' => 'http://', 'webcal://' => 'http://'));
if ($curl) {
$headers[] = 'User-Agent: Drupal (+http://drupal.org/)';
$result = new stdClass();
// Parse the URL and make sure we can handle the schema.
// cURL can only support either http:// or https://.
// CURLOPT_PROTOCOLS is only supported with cURL 7.19.4
$uri = parse_url($url);
if (!isset($uri['scheme'])) {
$result->error = 'missing schema';
$result->code = -1002;
}
else {
switch ($uri['scheme']) {
case 'http':
case 'https':
// Valid scheme.
break;
default:
$result->error = 'invalid schema ' . $uri['scheme'];
$result->code = -1003;
break;
}
}
// If the scheme was valid, continue to request the feed using cURL.
if (empty($result->error)) {
$download = curl_init($url);
curl_setopt($download, CURLOPT_FOLLOWLOCATION, TRUE);
if (!empty($username)) {
curl_setopt($download, CURLOPT_USERPWD, "{$username}:{$password}");
curl_setopt($download, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
}
curl_setopt($download, CURLOPT_HTTPHEADER, $headers);
curl_setopt($download, CURLOPT_HEADER, TRUE);
curl_setopt($download, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($download, CURLOPT_ENCODING, '');
curl_setopt($download, CURLOPT_TIMEOUT, variable_get('http_request_timeout', 30));
if ($accept_invalid_cert) {
curl_setopt($download, CURLOPT_SSL_VERIFYPEER, 0);
}
$header = '';
$data = curl_exec($download);
if (curl_error($download)) {
throw new HRCurlException(
t('cURL error (@code) @error for @url', array(
'@code' => curl_errno($download),
'@error' => curl_error($download),
'@url' => $url
)), curl_errno($download)
);
}
$header_size = curl_getinfo($download, CURLINFO_HEADER_SIZE);
$header = substr($data, 0, $header_size - 1);
$result->data = substr($data, $header_size);
$headers = preg_split("/(\r\n){2}/", $header);
$header_lines = preg_split("/\r\n|\n|\r/", end($headers));
$result->headers = array();
array_shift($header_lines); // skip HTTP response status
while ($line = trim(array_shift($header_lines))) {
list($header, $value) = explode(':', $line, 2);
// Normalize the headers.
$header = strtolower($header);
if (isset($result->headers[$header]) && $header == 'set-cookie') {
// RFC 2109: the Set-Cookie response header comprises the token Set-
// Cookie:, followed by a comma-separated list of one or more cookies.
$result->headers[$header] .= ',' . trim($value);
}
else {
$result->headers[$header] = trim($value);
}
}
$result->code = curl_getinfo($download, CURLINFO_HTTP_CODE);
curl_close($download);
}
}
else {
$result = drupal_http_request($url, array('headers' => $headers, 'timeout' => variable_get('http_request_timeout', 30)));
}
$result->code = isset($result->code) ? $result->code : 200;
// In case of 304 Not Modified try to return cached data.
if ($result->code == 304) {
if (isset($last_result)) {
$last_result->from_cache = TRUE;
return $last_result;
}
else {
// It's a tragedy, this file must exist and contain good data.
// In this case, clear cache and repeat.
cache_clear_all('feeds_http_download_' . md5($url), 'cache');
return http_request_get($url, $username, $password);
}
}
// Set caches.
cache_set('feeds_http_download_' . md5($url), $result);
$download_cache[$url] = $result;
return $result;
}
/**
* Decides if it's possible to use cURL or not.
*
* @return bool
* TRUE if curl is available, FALSE otherwise.
*/
function http_request_use_curl() {
// Allow site administrators to choose to not use cURL.
if (variable_get('feeds_never_use_curl', FALSE)) {
return FALSE;
}
// Check availability of cURL on the system.
$basedir = ini_get("open_basedir");
return function_exists('curl_init') && !ini_get('safe_mode') && empty($basedir);
}
/**
* Clear cache for a specific URL.
*/
function http_request_clear_cache($url) {
cache_clear_all('feeds_http_download_' . md5($url), 'cache');
}
/**
* Returns if the provided $content_type is a feed.
*
* @param string $content_type
* The Content-Type header.
*
* @param string $data
* The actual data from the http request.
*
* @return bool
* Returns TRUE if this is a parsable feed.
*/
function http_request_is_feed($content_type, $data) {
$pos = strpos($content_type, ';');
if ($pos !== FALSE) {
$content_type = substr($content_type, 0, $pos);
}
$content_type = strtolower($content_type);
if (strpos($content_type, 'xml') !== FALSE) {
return TRUE;
}
// @TODO: Sometimes the content-type can be text/html but still be a valid
// feed.
return FALSE;
}
/**
* Finds potential feed tags in the HTML document.
*
* @param string $html
* The html string to search.
*
* @return array
* An array of href to feeds.
*/
function http_request_find_feeds($html) {
$matches = array();
preg_match_all(HTTP_REQUEST_PCRE_LINK_TAG, $html, $matches);
$links = $matches[1];
$valid_links = array();
// Build up all the links information.
foreach ($links as $link_tag) {
$attributes = array();
$candidate = array();
preg_match_all(HTTP_REQUEST_PCRE_TAG_ATTRIBUTES, $link_tag, $attributes, PREG_SET_ORDER);
foreach ($attributes as $attribute) {
// Find the key value pairs, attribute[1] is key and attribute[2] is the
// value.
if (!empty($attribute[1]) && !empty($attribute[2])) {
$candidate[drupal_strtolower($attribute[1])] = drupal_strtolower(decode_entities($attribute[2]));
}
}
// Examine candidate to see if it s a feed.
// @TODO: could/should use http_request_is_feed ??
if (isset($candidate['rel']) && $candidate['rel'] == 'alternate') {
if (isset($candidate['href']) && isset($candidate['type']) && strpos($candidate['type'], 'xml') !== FALSE) {
// All tests pass, its a valid candidate.
$valid_links[] = $candidate['href'];
}
}
}
return $valid_links;
}
/**
* Create an absolute url.
*
* @param string $url
* The href to transform.
* @param string $base_url
* The url to be used as the base for a relative $url.
*
* @return string
* An absolute url
*/
function http_request_create_absolute_url($url, $base_url) {
$url = trim($url);
if (valid_url($url, TRUE)) {
// Valid absolute url already.
return $url;
}
// Turn relative url into absolute.
if (valid_url($url, FALSE)) {
// Produces variables $scheme, $host, $user, $pass, $path, $query and
// $fragment.
$parsed_url = parse_url($base_url);
$path = dirname($parsed_url['path']);
// Adding to the existing path.
if ($url{0} == '/') {
$cparts = array_filter(explode("/", $url));
}
else {
// Backtracking from the existing path.
$cparts = array_merge(array_filter(explode("/", $path)), array_filter(explode("/", $url)));
foreach ($cparts as $i => $part) {
if ($part == '.') {
$cparts[$i] = NULL;
}
if ($part == '..') {
$cparts[$i - 1] = NULL;
$cparts[$i] = NULL;
}
}
$cparts = array_filter($cparts);
}
$path = implode("/", $cparts);
// Build the prefix to the path.
$absolute_url = '';
if (isset($parsed_url['scheme'])) {
$absolute_url = $parsed_url['scheme'] . '://';
}
if (isset($parsed_url['user'])) {
$absolute_url .= $parsed_url['user'];
if (isset($pass)) {
$absolute_url .= ':' . $parsed_url['pass'];
}
$absolute_url .= '@';
}
if (isset($parsed_url['host'])) {
$absolute_url .= $parsed_url['host'] . '/';
}
$absolute_url .= $path;
if (valid_url($absolute_url, TRUE)) {
return $absolute_url;
}
}
return FALSE;
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* @file
* OPML Parser.
*/
/**
* Parse OPML file.
*
* @param $raw
* File contents.
* @return
* An array of the parsed OPML file.
*/
function opml_parser_parse($raw) {
$feeds = $items = array();
$xml = @ new SimpleXMLElement($raw);
$feeds['title'] = (string)current($xml->xpath('//head/title'));
// @todo Make xpath case insensitive.
$outlines = $xml->xpath('//outline[@xmlUrl]');
foreach ($outlines as $outline) {
$item = array();
foreach ($outline->attributes() as $k => $v) {
if (in_array(strtolower($k), array('title', 'text', 'xmlurl'))) {
$item[strtolower($k)] = (string) $v;
}
}
// If no title, forge it from text.
if (!isset($item['title']) && isset($item['text'])) {
if (strlen($item['text']) < 40) {
$item['title'] = $item['text'];
}
else {
$item['title'] = trim(substr($item['text'], 0, 30)) . ' ...';
}
}
if (isset($item['title']) && isset($item['xmlurl'])) {
$items[] = $item;
}
}
$feeds['items'] = $items;
return $feeds;
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @file
* On behalf implementation of Feeds mapping API for date
*/
/**
* Implements hook_feeds_processor_targets_alter().
*
* @see FeedsNodeProcessor::getMappingTargets().
*
* @todo Only provides "end date" target if field allows it.
*/
function date_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
$info = field_info_field($name);
if (in_array($info['type'], array('date', 'datestamp', 'datetime'))) {
$targets[$name . ':start'] = array(
'name' => t('@name: Start', array('@name' => $instance['label'])),
'callback' => 'date_feeds_set_target',
'description' => t('The start date for the @name field. Also use if mapping both start and end.', array('@name' => $instance['label'])),
'real_target' => $name,
);
$targets[$name . ':end'] = array(
'name' => t('@name: End', array('@name' => $instance['label'])),
'callback' => 'date_feeds_set_target',
'description' => t('The end date for the @name field.', array('@name' => $instance['label'])),
'real_target' => $name,
);
}
}
}
/**
* Implements hook_feeds_set_target().
*
* @param $node
* The target node.
* @param $field_name
* The name of field on the target node to map to.
* @param $feed_element
* The value to be mapped. Should be either a (flexible) date string
* or a FeedsDateTimeElement object.
*
* @todo Support array of values for dates.
*/
function date_feeds_set_target($source, $entity, $target, $feed_element) {
list($field_name, $sub_field) = explode(':', $target, 2);
if (!($feed_element instanceof FeedsDateTimeElement)) {
if (is_array($feed_element)) {
$feed_element = $feed_element[0];
}
if ($sub_field == 'end') {
$feed_element = new FeedsDateTimeElement(NULL, $feed_element);
}
else {
$feed_element = new FeedsDateTimeElement($feed_element, NULL);
}
}
$feed_element->buildDateField($entity, $field_name);
}

View File

@@ -0,0 +1,94 @@
<?php
/**
* @file
* On behalf implementation of Feeds mapping API for file.module and
* image.module.
*
* Does actually not include mappers for field types defined in fields module
* (because there aren't any) but mappers for all fields that contain their
* value simply in $entity->fieldname['und'][$i]['value'].
*/
/**
* Implements hook_feeds_processor_targets_alter().
*
* @see FeedsNodeProcessor::getMappingTargets().
*/
function file_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
$info = field_info_field($name);
if (in_array($info['type'], array('file', 'image'))) {
$targets[$name] = array(
'name' => check_plain($instance['label']),
'callback' => 'file_feeds_set_target',
'description' => t('The @label field of the node.', array('@label' => $instance['label'])),
);
}
}
}
/**
* Callback for mapping. Here is where the actual mapping happens.
*
* When the callback is invoked, $target contains the name of the field the
* user has decided to map to and $value contains the value of the feed item
* element the user has picked as a source.
*/
function file_feeds_set_target($source, $entity, $target, $value) {
if (empty($value)) {
return;
}
module_load_include('inc', 'file');
// Make sure $value is an array of objects of type FeedsEnclosure.
if (!is_array($value)) {
$value = array($value);
}
foreach ($value as $k => $v) {
if (!($v instanceof FeedsEnclosure)) {
if (is_string($v)) {
$value[$k] = new FeedsEnclosure($v, file_get_mimetype($v));
}
else {
unset($value[$k]);
}
}
}
if (empty($value)) {
return;
}
// Determine file destination.
// @todo This needs review and debugging.
list($entity_id, $vid, $bundle_name) = entity_extract_ids($entity->feeds_item->entity_type, $entity);
$instance_info = field_info_instance($entity->feeds_item->entity_type, $target, $bundle_name);
$info = field_info_field($target);
$data = array();
if (!empty($entity->uid)) {
$data[$entity->feeds_item->entity_type] = $entity;
}
$destination = file_field_widget_uri($info, $instance_info, $data);
// Populate entity.
$i = 0;
$field = isset($entity->$target) ? $entity->$target : array();
foreach ($value as $v) {
try {
$file = $v->getFile($destination);
}
catch (Exception $e) {
watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
}
if ($file) {
$field['und'][$i] = (array)$file;
$field['und'][$i]['display'] = 1; // @todo: Figure out how to properly populate this field.
if ($info['cardinality'] == 1) {
break;
}
$i++;
}
}
$entity->{$target} = $field;
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* @file
* On behalf implementation of Feeds mapping API for link.module.
*/
/**
* Implements hook_feeds_processor_targets_alter().
*
* @see FeedsProcessor::getMappingTargets()
*/
function link_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
$info = field_info_field($name);
if ($info['type'] == 'link_field') {
if (array_key_exists('url', $info['columns'])) {
$targets[$name . ':url'] = array(
'name' => t('@name: URL', array('@name' => $instance['label'])),
'callback' => 'link_feeds_set_target',
'description' => t('The @label field of the entity.', array('@label' => $instance['label'])),
'real_target' => $name,
);
}
if (array_key_exists('title', $info['columns'])) {
$targets[$name . ':title'] = array(
'name' => t('@name: Title', array('@name' => $instance['label'])),
'callback' => 'link_feeds_set_target',
'description' => t('The @label field of the entity.', array('@label' => $instance['label'])),
'real_target' => $name,
);
}
}
}
}
/**
* Callback for mapping. Here is where the actual mapping happens.
*
* When the callback is invoked, $target contains the name of the field the
* user has decided to map to and $value contains the value of the feed item
* element the user has picked as a source.
*/
function link_feeds_set_target($source, $entity, $target, $value) {
if (empty($value)) {
return;
}
// Handle non-multiple value fields.
if (!is_array($value)) {
$value = array($value);
}
// Iterate over all values.
list($field_name, $column) = explode(':', $target);
$info = field_info_field($field_name);
$field = isset($entity->$field_name) ? $entity->$field_name : array();
$delta = 0;
foreach ($value as $v) {
if ($info['cardinality'] == $delta) {
break;
}
if (is_object($v) && ($v instanceof FeedsElement)) {
$v = $v->getValue();
}
if (is_scalar($v)) {
$field['und'][$delta][$column] = $v;
$delta++;
}
}
$entity->$field_name = $field;
}

View File

@@ -0,0 +1,74 @@
<?php
/**
* @file
* On behalf implementation of Feeds mapping API for number.module.
*/
/**
* Implements hook_feeds_processor_targets_alter().
*
* @see FeedsProcessor::getMappingTargets()
*/
function number_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
$numeric_types = array(
'list_integer',
'list_float',
'list_boolean',
'number_integer',
'number_decimal',
'number_float',
);
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
$info = field_info_field($name);
if (in_array($info['type'], $numeric_types)) {
$targets[$name] = array(
'name' => check_plain($instance['label']),
'callback' => 'number_feeds_set_target',
'description' => t('The @label field of the entity.', array('@label' => $instance['label'])),
);
}
}
}
/**
* Callback for mapping numerics.
*
* Ensure that $value is a numeric to avoid database errors.
*/
function number_feeds_set_target($source, $entity, $target, $value) {
// Do not perform the regular empty() check here. 0 is a valid value. That's
// really just a performance thing anyway.
if (!is_array($value)) {
$value = array($value);
}
$info = field_info_field($target);
// Iterate over all values.
$field = isset($entity->$target) ? $entity->$target : array('und' => array());
// Allow for multiple mappings to the same target.
$delta = count($field['und']);
foreach ($value as $v) {
if ($info['cardinality'] == $delta) {
break;
}
if (is_object($v) && ($v instanceof FeedsElement)) {
$v = $v->getValue();
}
if (is_numeric($v)) {
$field['und'][$delta]['value'] = $v;
$delta++;
}
}
$entity->$target = $field;
}

View File

@@ -0,0 +1,120 @@
<?php
/**
* @file
* On behalf implementation of Feeds mapping API for path.module.
*/
/**
* Implements hook_feeds_processor_targets_alter().
*
* @see FeedsNodeProcessor::getMappingTargets().
*/
function path_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
switch ($entity_type) {
case 'node':
case 'taxonomy_term':
case 'user':
$targets['path_alias'] = array(
'name' => t('Path alias'),
'description' => t('URL path alias of the node.'),
'callback' => 'path_feeds_set_target',
'summary_callback' => 'path_feeds_summary_callback',
'form_callback' => 'path_feeds_form_callback',
);
break;
}
}
/**
* Callback for mapping. Here is where the actual mapping happens.
*
* When the callback is invoked, $target contains the name of the field the
* user has decided to map to and $value contains the value of the feed item
* element the user has picked as a source.
*/
function path_feeds_set_target($source, $entity, $target, $value, $mapping) {
if (empty($value)) {
$value = '';
}
// Path alias cannot be multi-valued, so use the first value.
if (is_array($value)) {
$value = $value[0];
}
$entity->path = array();
$entity_type = $source->importer->processor->entityType();
list($id, , ) = entity_extract_ids($entity_type, $entity);
if ($id) {
$uri = entity_uri($entity_type, $entity);
// Check for existing aliases.
if ($path = path_load($uri['path'])) {
$entity->path = $path;
}
}
$entity->path['pathauto'] = FALSE;
// Allow pathauto to set the path alias if the option is set, and this value
// is empty.
if (!empty($mapping['pathauto_override']) && !$value) {
$entity->path['pathauto'] = TRUE;
}
else {
$entity->path['alias'] = ltrim($value, '/');
}
}
/**
* Mapping configuration summary for path.module.
*
* @param $mapping
* Associative array of the mapping settings.
* @param $target
* Array of target settings, as defined by the processor or
* hook_feeds_processor_targets_alter().
* @param $form
* The whole mapping form.
* @param $form_state
* The form state of the mapping form.
*
* @return
* Returns, as a string that may contain HTML, the summary to display while
* the full form isn't visible.
* If the return value is empty, no summary and no option to view the form
* will be displayed.
*/
function path_feeds_summary_callback($mapping, $target, $form, $form_state) {
if (!module_exists('pathauto')) {
return;
}
if (empty($mapping['pathauto_override'])) {
return t('Do not allow Pathauto if empty.');
}
else {
return t('Allow Pathauto if empty.');
}
}
/**
* Settings form callback.
*
* @return
* The per mapping configuration form. Once the form is saved, $mapping will
* be populated with the form values.
*/
function path_feeds_form_callback($mapping, $target, $form, $form_state) {
return array(
'pathauto_override' => array(
'#type' => 'checkbox',
'#title' => t('Allow Pathauto to set the alias if the value is empty.'),
'#default_value' => !empty($mapping['pathauto_override']),
),
);
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* On behalf implementation of Feeds mapping API for user profiles.
*/
/**
* Implements hook_feeds_processor_target_alter().
*/
function profile_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
if ($entity_type != 'user') {
return;
}
$categories = profile_user_categories();
foreach ($categories as $category) {
foreach (_profile_get_fields($category['name']) as $record) {
$targets[$record->name] = array(
'name' => t('Profile: @name', array('@name' => $record->title)),
'description' => t('Profile: @name', array('@name' => $record->title)),
'callback' => 'profile_feeds_set_target',
);
}
}
}
/**
* Set the user profile target after import.
*/
function profile_feeds_set_target($source, $entity, $target, $value, $mapping) {
$entity->$target = $value;
}

View File

@@ -0,0 +1,186 @@
<?php
/**
* @file
* Mapper that exposes a node's taxonomy vocabularies as mapping targets.
*/
/**
* Implements hook_feeds_parser_sources_alter().
*
* @todo: Upgrade to 7.
*/
function taxonomy_feeds_parser_sources_alter(&$sources, $content_type) {
if (!empty($content_type)) {
foreach (taxonomy_get_vocabularies($content_type) as $vocabulary) {
$sources['parent:taxonomy:' . $vocabulary->machine_name] = array(
'name' => t('Feed node: Taxonomy: @vocabulary', array('@vocabulary' => $vocabulary->name)),
'description' => t('Taxonomy terms from feed node in given vocabulary.'),
'callback' => 'taxonomy_feeds_get_source',
);
}
}
}
/**
* Callback, returns taxonomy from feed node.
*/
function taxonomy_feeds_get_source(FeedsSource $source, FeedsParserResult $result, $key) {
if ($node = node_load($source->feed_nid)) {
$terms = taxonomy_feeds_node_get_terms($node);
$vocabularies = taxonomy_vocabulary_load_multiple(array(), array('machine_name' => str_replace('parent:taxonomy:', '', $key)));
$vocabulary = array_shift($vocabularies);
$result = array();
foreach ($terms as $tid => $term) {
if ($term->vid == $vocabulary->vid) {
$result[] = new FeedsTermElement($term);
}
}
return $result;
}
}
/**
* Implements hook_feeds_processor_targets_alter().
*/
function taxonomy_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
$info = field_info_field($name);
if ($info['type'] == 'taxonomy_term_reference') {
$targets[$name] = array(
'name' => check_plain($instance['label']),
'callback' => 'taxonomy_feeds_set_target',
'description' => t('The @label field of the node.', array('@label' => $instance['label'])),
);
}
}
}
/**
* Callback for mapping. Here is where the actual mapping happens.
*
* @todo Do not create new terms for non-autotag fields.
*/
function taxonomy_feeds_set_target($source, $entity, $target, $terms) {
if (empty($terms)) {
return;
}
// Handle non-multiple values.
if (!is_array($terms)) {
$terms = array($terms);
}
$info = field_info_field($target);
// See http://drupal.org/node/881530
if (isset($info['settings']['allowed_values'][0]['vocabulary'])) {
$vocabulary = taxonomy_vocabulary_machine_name_load($info['settings']['allowed_values'][0]['vocabulary']);
}
else {
$vocabulary = taxonomy_vocabulary_load($info['settings']['allowed_values'][0]['vid']);
}
$i = 0;
$entity->$target = isset($entity->$target) ? $entity->$target : array();
foreach ($terms as $term) {
$tid = 0;
if ($term instanceof FeedsTermElement) {
$tid = $term->tid;
}
elseif (is_numeric($term)) {
$tid = $term;
}
elseif (is_string($term)) {
$tid = taxonomy_term_check_term($term, $vocabulary->vid);
}
if ($tid) {
$entity->{$target}['und'][$i]['tid'] = $tid;
}
if ($info['cardinality'] == 1) {
break;
}
$i++;
}
}
/**
* Find all terms associated with the given node, within one vocabulary.
*/
function taxonomy_feeds_node_get_terms($node, $key = 'tid') {
$terms = &drupal_static(__FUNCTION__);
if (!isset($terms[$node->nid][$key])) {
// Get tids from all taxonomy_term_reference fields.
$tids = array();
$fields = field_info_fields();
foreach ($fields as $field_name => $field) {
if ($field['type'] == 'taxonomy_term_reference' && field_info_instance('node', $field_name, $node->type)) {
if (($items = field_get_items('node', $node, $field_name)) && is_array($items)) {
$tids = array_merge($tids, array_map('_taxonomy_extract_tid', $items));
}
}
}
// Load terms and cache them in static var.
$curr_terms = taxonomy_term_load_multiple($tids);
$terms[$node->nid][$key] = array();
foreach ($curr_terms as $term) {
$terms[$node->nid][$key][$term->$key] = $term;
}
}
return $terms[$node->nid][$key];
}
/**
* Helper function used in taxonomy_feeds_node_get_terms(). Extracts
* tid from array item returned by field_get_items().
*
* @param $item tid information in a form of single element array (key == 'tid', value == tid we're looking for)
*
* @return tid extracted from $item.
*
* @see taxonomy_feeds_node_get_terms()
* @see field_get_items()
*/
function _taxonomy_extract_tid($item) {
return $item['tid'];
}
/**
* Checks whether a term identified by name and vocabulary exists. Creates a
* new term if it does not exist.
*
* @param $name
* A term name.
* @param $vid
* A vocabulary id.
*
* @return
* A term id.
*/
function taxonomy_term_check_term($name, $vid) {
$name = trim($name);
$term = taxonomy_term_lookup_term($name, $vid);
if (empty($term)) {
$term = new stdClass();
$term->name = $name;
$term->vid = $vid;
taxonomy_term_save($term);
return $term->tid;
}
return $term->tid;
}
/**
* Looks up a term, assumes SQL storage backend.
*/
function taxonomy_term_lookup_term($name, $vid) {
return db_select('taxonomy_term_data', 'td')
->fields('td', array('tid', 'name'))
->condition('name', $name)
->condition('vid', $vid)
->execute()
->fetchObject();
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* @file
* On behalf implementation of Feeds mapping API for text.module.
*/
/**
* Implements hook_feeds_processor_targets_alter().
*
* @see FeedsProcessor::getMappingTargets()
*/
function text_feeds_processor_targets_alter(&$targets, $entity_type, $bundle_name) {
$text_types = array(
'list_text',
'text',
'text_long',
'text_with_summary',
);
foreach (field_info_instances($entity_type, $bundle_name) as $name => $instance) {
$info = field_info_field($name);
if (in_array($info['type'], $text_types)) {
$targets[$name] = array(
'name' => check_plain($instance['label']),
'callback' => 'text_feeds_set_target',
'description' => t('The @label field of the entity.', array('@label' => $instance['label'])),
);
}
}
}
/**
* Callback for mapping text fields.
*/
function text_feeds_set_target($source, $entity, $target, $value) {
if (empty($value)) {
return;
}
if (!is_array($value)) {
$value = array($value);
}
if (isset($source->importer->processor->config['input_format'])) {
$format = $source->importer->processor->config['input_format'];
}
$info = field_info_field($target);
// Iterate over all values.
$field = isset($entity->$target) ? $entity->$target : array('und' => array());
// Allow for multiple mappings to the same target.
$delta = count($field['und']);
foreach ($value as $v) {
if ($info['cardinality'] == $delta) {
break;
}
if (is_object($v) && ($v instanceof FeedsElement)) {
$v = $v->getValue();
}
if (is_scalar($v)) {
$field['und'][$delta]['value'] = $v;
if (isset($format)) {
$field['und'][$delta]['format'] = $format;
}
$delta++;
}
}
$entity->$target = $field;
}

View File

@@ -0,0 +1,216 @@
<?php
/**
* @file
* Contains the FeedsCSVParser class.
*/
/**
* Parses a given file as a CSV file.
*/
class FeedsCSVParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
$source_config = $source->getConfigFor($this);
$state = $source->state(FEEDS_PARSE);
// Load and configure parser.
feeds_include_library('ParserCSV.inc', 'ParserCSV');
$parser = new ParserCSV();
$delimiter = $source_config['delimiter'] == 'TAB' ? "\t" : $source_config['delimiter'];
$parser->setDelimiter($delimiter);
$iterator = new ParserCSVIterator($fetcher_result->getFilePath());
if (empty($source_config['no_headers'])) {
// Get first line and use it for column names, convert them to lower case.
$header = $this->parseHeader($parser, $iterator);
if (!$header) {
return;
}
$parser->setColumnNames($header);
}
// Determine section to parse, parse.
$start = $state->pointer ? $state->pointer : $parser->lastLinePos();
$limit = $source->importer->getLimit();
$rows = $this->parseItems($parser, $iterator, $start, $limit);
// Report progress.
$state->total = filesize($fetcher_result->getFilePath());
$state->pointer = $parser->lastLinePos();
$progress = $parser->lastLinePos() ? $parser->lastLinePos() : $state->total;
$state->progress($state->total, $progress);
// Create a result object and return it.
return new FeedsParserResult($rows, $source->feed_nid);
}
/**
* Get first line and use it for column names, convert them to lower case.
* Be aware that the $parser and iterator objects can be modified in this
* function since they are passed in by reference
*
* @param ParserCSV $parser
* @param ParserCSVIterator $iterator
* @return
* An array of lower-cased column names to use as keys for the parsed items.
*/
protected function parseHeader(ParserCSV $parser, ParserCSVIterator $iterator) {
$parser->setLineLimit(1);
$rows = $parser->parse($iterator);
if (!count($rows)) {
return FALSE;
}
$header = array_shift($rows);
foreach ($header as $i => $title) {
$header[$i] = trim(drupal_strtolower($title));
}
return $header;
}
/**
* Parse all of the items from the CSV.
*
* @param ParserCSV $parser
* @param ParserCSVIterator $iterator
* @return
* An array of rows of the CSV keyed by the column names previously set
*/
protected function parseItems(ParserCSV $parser, ParserCSVIterator $iterator, $start = 0, $limit = 0) {
$parser->setLineLimit($limit);
$parser->setStartByte($start);
$rows = $parser->parse($iterator);
return $rows;
}
/**
* Override parent::getMappingSources().
*/
public function getMappingSources() {
return FALSE;
}
/**
* Override parent::getSourceElement() to use only lower keys.
*/
public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
return parent::getSourceElement($source, $result, drupal_strtolower($element_key));
}
/**
* Define defaults.
*/
public function sourceDefaults() {
return array(
'delimiter' => $this->config['delimiter'],
'no_headers' => $this->config['no_headers'],
);
}
/**
* Source form.
*
* Show mapping configuration as a guidance for import form users.
*/
public function sourceForm($source_config) {
$form = array();
$form['#weight'] = -10;
$mappings = feeds_importer($this->id)->processor->config['mappings'];
$sources = $uniques = array();
foreach ($mappings as $mapping) {
$sources[] = check_plain($mapping['source']);
if ($mapping['unique']) {
$uniques[] = check_plain($mapping['source']);
}
}
$output = t('Import !csv_files with one or more of these columns: !columns.', array('!csv_files' => l(t('CSV files'), 'http://en.wikipedia.org/wiki/Comma-separated_values'), '!columns' => implode(', ', $sources)));
$items = array();
$items[] = format_plural(count($uniques), t('Column <strong>!column</strong> is mandatory and considered unique: only one item per !column value will be created.', array('!column' => implode(', ', $uniques))), t('Columns <strong>!columns</strong> are mandatory and values in these columns are considered unique: only one entry per value in one of these column will be created.', array('!columns' => implode(', ', $uniques))));
$items[] = l(t('Download a template'), 'import/' . $this->id . '/template');
$form['help']['#markup'] = '<div class="help"><p>' . $output . '</p>' . theme('item_list', array('items' => $items)) . '</div>';
$form['delimiter'] = array(
'#type' => 'select',
'#title' => t('Delimiter'),
'#description' => t('The character that delimits fields in the CSV file.'),
'#options' => array(
',' => ',',
';' => ';',
'TAB' => 'TAB',
),
'#default_value' => isset($source_config['delimiter']) ? $source_config['delimiter'] : ',',
);
$form['no_headers'] = array(
'#type' => 'checkbox',
'#title' => t('No Headers'),
'#description' => t('Check if the imported CSV file does not start with a header row. If checked, mapping sources must be named \'0\', \'1\', \'2\' etc.'),
'#default_value' => isset($source_config['no_headers']) ? $source_config['no_headers'] : 0,
);
return $form;
}
/**
* Define default configuration.
*/
public function configDefaults() {
return array(
'delimiter' => ',',
'no_headers' => 0,
);
}
/**
* Build configuration form.
*/
public function configForm(&$form_state) {
$form = array();
$form['delimiter'] = array(
'#type' => 'select',
'#title' => t('Default delimiter'),
'#description' => t('Default field delimiter.'),
'#options' => array(
',' => ',',
';' => ';',
'TAB' => 'TAB',
),
'#default_value' => $this->config['delimiter'],
);
$form['no_headers'] = array(
'#type' => 'checkbox',
'#title' => t('No headers'),
'#description' => t('Check if the imported CSV file does not start with a header row. If checked, mapping sources must be named \'0\', \'1\', \'2\' etc.'),
'#default_value' => $this->config['no_headers'],
);
return $form;
}
public function getTemplate() {
$mappings = feeds_importer($this->id)->processor->config['mappings'];
$sources = $uniques = array();
foreach ($mappings as $mapping) {
if ($mapping['unique']) {
$uniques[] = check_plain($mapping['source']);
}
else {
$sources[] = check_plain($mapping['source']);
}
}
$sep = ',';
$columns = array();
foreach (array_merge($uniques, $sources) as $col) {
if (strpos($col, $sep) !== FALSE) {
$col = '"' . str_replace('"', '""', $col) . '"';
}
$columns[] = $col;
}
drupal_add_http_header('Cache-Control', 'max-age=60, must-revalidate');
drupal_add_http_header('Content-Disposition', 'attachment; filename="' . $this->id . '_template.csv"');
drupal_add_http_header('Content-type', 'text/csv; charset=utf-8');
print implode($sep, $columns);
return;
}
}

View File

@@ -0,0 +1,218 @@
<?php
/**
* @file
* Contains the FeedsFetcher and related classes.
*/
/**
* Base class for all fetcher results.
*/
class FeedsFetcherResult extends FeedsResult {
protected $raw;
protected $file_path;
/**
* Constructor.
*/
public function __construct($raw) {
$this->raw = $raw;
}
/**
* @return
* The raw content from the source as a string.
*
* @throws Exception
* Extending classes MAY throw an exception if a problem occurred.
*/
public function getRaw() {
return $this->sanitizeRaw($this->raw);
}
/**
* Get a path to a temporary file containing the resource provided by the
* fetcher.
*
* File will be deleted after DRUPAL_MAXIMUM_TEMP_FILE_AGE.
*
* @return
* A path to a file containing the raw content as a source.
*
* @throws Exception
* If an unexpected problem occurred.
*/
public function getFilePath() {
if (!isset($this->file_path)) {
$destination = 'public://feeds';
if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
throw new Exception(t('Feeds directory either cannot be created or is not writable.'));
}
$this->file_path = FALSE;
if ($file = file_save_data($this->getRaw(), $destination . '/' . get_class($this) . REQUEST_TIME)) {
$file->status = 0;
file_save($file);
$this->file_path = $file->uri;
}
else {
throw new Exception(t('Cannot write content to %dest', array('%dest' => $destination)));
}
}
return $this->sanitizeFile($this->file_path);
}
/**
* Sanitize the raw content string. Currently supported sanitizations:
*
* - Remove BOM header from UTF-8 files.
*
* @param string $raw
* The raw content string to be sanitized.
* @return
* The sanitized content as a string.
*/
public function sanitizeRaw($raw) {
if (substr($raw, 0, 3) == pack('CCC', 0xef, 0xbb, 0xbf)) {
$raw = substr($raw, 3);
}
return $raw;
}
/**
* Sanitize the file in place. Currently supported sanitizations:
*
* - Remove BOM header from UTF-8 files.
*
* @param string $filepath
* The file path of the file to be sanitized.
* @return
* The file path of the sanitized file.
*/
public function sanitizeFile($filepath) {
$handle = fopen($filepath, 'r');
$line = fgets($handle);
fclose($handle);
// If BOM header is present, read entire contents of file and overwrite
// the file with corrected contents.
if (substr($line, 0, 3) == pack('CCC', 0xef, 0xbb, 0xbf)) {
$contents = file_get_contents($filepath);
$contents = substr($contents, 3);
$status = file_put_contents($filepath, $contents);
if ($status === FALSE) {
throw new Exception(t('File @filepath is not writeable.', array('@filepath' => $filepath)));
}
}
return $filepath;
}
}
/**
* Abstract class, defines shared functionality between fetchers.
*
* Implements FeedsSourceInfoInterface to expose source forms to Feeds.
*/
abstract class FeedsFetcher extends FeedsPlugin {
/**
* Fetch content from a source and return it.
*
* Every class that extends FeedsFetcher must implement this method.
*
* @param $source
* Source value as entered by user through sourceForm().
*
* @return
* A FeedsFetcherResult object.
*/
public abstract function fetch(FeedsSource $source);
/**
* Clear all caches for results for given source.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers can choose to only clear
* caches pertaining to this source.
*/
public function clear(FeedsSource $source) {}
/**
* Request handler invoked if callback URL is requested. Locked down by
* default. For a example usage see FeedsHTTPFetcher.
*
* Note: this method may exit the script.
*
* @return
* A string to be returned to the client.
*/
public function request($feed_nid = 0) {
drupal_access_denied();
}
/**
* Construct a path for a concrete fetcher/source combination. The result of
* this method matches up with the general path definition in
* FeedsFetcher::menuItem(). For example usage look at FeedsHTTPFetcher.
*
* @return
* Path for this fetcher/source combination.
*/
public function path($feed_nid = 0) {
$id = urlencode($this->id);
if ($feed_nid && is_numeric($feed_nid)) {
return "feeds/importer/$id/$feed_nid";
}
return "feeds/importer/$id";
}
/**
* Menu item definition for fetchers of this class. Note how the path
* component in the item definition matches the return value of
* FeedsFetcher::path();
*
* Requests to this menu item will be routed to FeedsFetcher::request().
*
* @return
* An array where the key is the Drupal menu item path and the value is
* a valid Drupal menu item definition.
*/
public function menuItem() {
return array(
'feeds/importer/%feeds_importer' => array(
'page callback' => 'feeds_fetcher_callback',
'page arguments' => array(2, 3),
'access callback' => TRUE,
'file' => 'feeds.pages.inc',
'type' => MENU_CALLBACK,
),
);
}
/**
* Subscribe to a source. Only implement if fetcher requires subscription.
*
* @param FeedsSource $source
* Source information for this subscription.
*/
public function subscribe(FeedsSource $source) {}
/**
* Unsubscribe from a source. Only implement if fetcher requires subscription.
*
* @param FeedsSource $source
* Source information for unsubscribing.
*/
public function unsubscribe(FeedsSource $source) {}
/**
* Override import period settings. This can be used to force a certain import
* interval.
*
* @param $source
* A FeedsSource object.
*
* @return
* A time span in seconds if periodic import should be overridden for given
* $source, NULL otherwise.
*/
public function importPeriod(FeedsSource $source) {}
}

View File

@@ -0,0 +1,229 @@
<?php
/**
* @file
* Home of the FeedsFileFetcher and related classes.
*/
/**
* Definition of the import batch object created on the fetching stage by
* FeedsFileFetcher.
*/
class FeedsFileFetcherResult extends FeedsFetcherResult {
/**
* Constructor.
*/
public function __construct($file_path) {
parent::__construct('');
$this->file_path = $file_path;
}
/**
* Overrides parent::getRaw();
*/
public function getRaw() {
return $this->sanitizeRaw(file_get_contents($this->file_path));
}
/**
* Overrides parent::getFilePath().
*/
public function getFilePath() {
if (!file_exists($this->file_path)) {
throw new Exception(t('File @filepath is not accessible.', array('@filepath' => $this->file_path)));
}
return $this->sanitizeFile($this->file_path);
}
}
/**
* Fetches data via HTTP.
*/
class FeedsFileFetcher extends FeedsFetcher {
/**
* Implements FeedsFetcher::fetch().
*/
public function fetch(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
// Just return a file fetcher result if this is a file.
if (is_file($source_config['source'])) {
return new FeedsFileFetcherResult($source_config['source']);
}
// Batch if this is a directory.
$state = $source->state(FEEDS_FETCH);
$files = array();
if (!isset($state->files)) {
$state->files = $this->listFiles($source_config['source']);
$state->total = count($state->files);
}
if (count($state->files)) {
$file = array_shift($state->files);
$state->progress($state->total, $state->total - count($state->files));
return new FeedsFileFetcherResult($file);
}
throw new Exception(t('Resource is not a file or it is an empty directory: %source', array('%source' => $source_config['source'])));
}
/**
* Return an array of files in a directory.
*
* @param $dir
* A stream wreapper URI that is a directory.
*
* @return
* An array of stream wrapper URIs pointing to files. The array is empty
* if no files could be found. Never contains directories.
*/
protected function listFiles($dir) {
$dir = file_stream_wrapper_uri_normalize($dir);
$files = array();
if ($items = @scandir($dir)) {
foreach ($items as $item) {
if (is_file("$dir/$item") && strpos($item, '.') !== 0) {
$files[] = "$dir/$item";
}
}
}
return $files;
}
/**
* Source form.
*/
public function sourceForm($source_config) {
$form = array();
$form['fid'] = array(
'#type' => 'value',
'#value' => empty($source_config['fid']) ? 0 : $source_config['fid'],
);
if (empty($this->config['direct'])) {
$form['source'] = array(
'#type' => 'value',
'#value' => empty($source_config['source']) ? '' : $source_config['source'],
);
$form['upload'] = array(
'#type' => 'file',
'#title' => empty($this->config['direct']) ? t('File') : NULL,
'#description' => empty($source_config['source']) ? t('Select a file from your local system.') : t('Select a different file from your local system.'),
'#theme' => 'feeds_upload',
'#file_info' => empty($source_config['fid']) ? NULL : file_load($source_config['fid']),
'#size' => 10,
);
}
else {
$form['source'] = array(
'#type' => 'textfield',
'#title' => t('File'),
'#description' => t('Specify a path to a file or a directory. Path must start with @scheme://', array('@scheme' => file_default_scheme())),
'#default_value' => empty($source_config['source']) ? '' : $source_config['source'],
);
}
return $form;
}
/**
* Override parent::sourceFormValidate().
*/
public function sourceFormValidate(&$values) {
$values['source'] = trim($values['source']);
$feed_dir = 'public://feeds';
file_prepare_directory($feed_dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
// If there is a file uploaded, save it, otherwise validate input on
// file.
// @todo: Track usage of file, remove file when removing source.
if ($file = file_save_upload('feeds', array('file_validate_extensions' => array(0 => $this->config['allowed_extensions'])), $feed_dir)) {
$values['source'] = $file->uri;
$values['file'] = $file;
}
elseif (empty($values['source'])) {
form_set_error('feeds][source', t('Upload a file first.'));
}
// If a file has not been uploaded and $values['source'] is not empty, make
// sure that this file is within Drupal's files directory as otherwise
// potentially any file that the web server has access to could be exposed.
elseif (strpos($values['source'], file_default_scheme()) !== 0) {
form_set_error('feeds][source', t('File needs to reside within the site\'s file directory, its path needs to start with @scheme://.', array('@scheme' => file_default_scheme())));
}
}
/**
* Override parent::sourceSave().
*/
public function sourceSave(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
// If a new file is present, delete the old one and replace it with the new
// one.
if (isset($source_config['file'])) {
$file = $source_config['file'];
if (isset($source_config['fid'])) {
$this->deleteFile($source_config['fid'], $source->feed_nid);
}
$file->status = FILE_STATUS_PERMANENT;
file_save($file);
file_usage_add($file, 'feeds', get_class($this), $source->feed_nid);
$source_config['fid'] = $file->fid;
unset($source_config['file']);
$source->setConfigFor($this, $source_config);
}
}
/**
* Override parent::sourceDelete().
*/
public function sourceDelete(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
if (isset($source_config['fid'])) {
$this->deleteFile($source_config['fid'], $source->feed_nid);
}
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'allowed_extensions' => 'txt csv tsv xml opml',
'direct' => FALSE,
);
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$form = array();
$form['allowed_extensions'] = array(
'#type' => 'textfield',
'#title' => t('Allowed file extensions'),
'#description' => t('Allowed file extensions for upload.'),
'#default_value' => $this->config['allowed_extensions'],
);
$form['direct'] = array(
'#type' => 'checkbox',
'#title' => t('Supply path to file or directory directly'),
'#description' => t('For experts. Lets users specify a path to a file <em>or a directory of files</em> directly,
instead of a file upload through the browser. This is useful when the files that need to be imported
are already on the server.'),
'#default_value' => $this->config['direct'],
);
return $form;
}
/**
* Helper. Deletes a file.
*/
protected function deleteFile($fid, $feed_nid) {
if ($file = file_load($fid)) {
file_usage_delete($file, 'feeds', get_class($this), $feed_nid);
file_delete($file);
}
}
}

View File

@@ -0,0 +1,332 @@
<?php
/**
* @file
* Home of the FeedsHTTPFetcher and related classes.
*/
feeds_include_library('PuSHSubscriber.inc', 'PuSHSubscriber');
/**
* Result of FeedsHTTPFetcher::fetch().
*/
class FeedsHTTPFetcherResult extends FeedsFetcherResult {
protected $url;
protected $file_path;
/**
* Constructor.
*/
public function __construct($url = NULL) {
$this->url = $url;
parent::__construct('');
}
/**
* Overrides FeedsFetcherResult::getRaw();
*/
public function getRaw() {
feeds_include_library('http_request.inc', 'http_request');
$result = http_request_get($this->url);
if (!in_array($result->code, array(200, 201, 202, 203, 204, 205, 206))) {
throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->url, '!code' => $result->code)));
}
return $this->sanitizeRaw($result->data);
}
}
/**
* Fetches data via HTTP.
*/
class FeedsHTTPFetcher extends FeedsFetcher {
/**
* Implements FeedsFetcher::fetch().
*/
public function fetch(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
if ($this->config['use_pubsubhubbub'] && ($raw = $this->subscriber($source->feed_nid)->receive())) {
return new FeedsFetcherResult($raw);
}
return new FeedsHTTPFetcherResult($source_config['source']);
}
/**
* Clear caches.
*/
public function clear(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
$url = $source_config['source'];
feeds_include_library('http_request.inc', 'http_request');
http_request_clear_cache($url);
}
/**
* Implements FeedsFetcher::request().
*/
public function request($feed_nid = 0) {
feeds_dbg($_GET);
@feeds_dbg(file_get_contents('php://input'));
// A subscription verification has been sent, verify.
if (isset($_GET['hub_challenge'])) {
$this->subscriber($feed_nid)->verifyRequest();
}
// No subscription notification has ben sent, we are being notified.
else {
try {
feeds_source($this->id, $feed_nid)->existing()->import();
}
catch (Exception $e) {
// In case of an error, respond with a 503 Service (temporary) unavailable.
header('HTTP/1.1 503 "Not Found"', NULL, 503);
drupal_exit();
}
}
// Will generate the default 200 response.
header('HTTP/1.1 200 "OK"', NULL, 200);
drupal_exit();
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'auto_detect_feeds' => FALSE,
'use_pubsubhubbub' => FALSE,
'designated_hub' => '',
);
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$form = array();
$form['auto_detect_feeds'] = array(
'#type' => 'checkbox',
'#title' => t('Auto detect feeds'),
'#description' => t('If the supplied URL does not point to a feed but an HTML document, attempt to extract a feed URL from the document.'),
'#default_value' => $this->config['auto_detect_feeds'],
);
$form['use_pubsubhubbub'] = array(
'#type' => 'checkbox',
'#title' => t('Use PubSubHubbub'),
'#description' => t('Attempt to use a <a href="http://en.wikipedia.org/wiki/PubSubHubbub">PubSubHubbub</a> subscription if available.'),
'#default_value' => $this->config['use_pubsubhubbub'],
);
$form['designated_hub'] = array(
'#type' => 'textfield',
'#title' => t('Designated hub'),
'#description' => t('Enter the URL of a designated PubSubHubbub hub (e. g. superfeedr.com). If given, this hub will be used instead of the hub specified in the actual feed.'),
'#default_value' => $this->config['designated_hub'],
'#dependency' => array(
'edit-use-pubsubhubbub' => array(1),
),
);
return $form;
}
/**
* Expose source form.
*/
public function sourceForm($source_config) {
$form = array();
$form['source'] = array(
'#type' => 'textfield',
'#title' => t('URL'),
'#description' => t('Enter a feed URL.'),
'#default_value' => isset($source_config['source']) ? $source_config['source'] : '',
'#maxlength' => NULL,
'#required' => TRUE,
);
return $form;
}
/**
* Override parent::sourceFormValidate().
*/
public function sourceFormValidate(&$values) {
$values['source'] = trim($values['source']);
if (!feeds_valid_url($values['source'], TRUE)) {
$form_key = 'feeds][' . get_class($this) . '][source';
form_set_error($form_key, t('The URL %source is invalid.', array('%source' => $values['source'])));
}
elseif ($this->config['auto_detect_feeds']) {
feeds_include_library('http_request.inc', 'http_request');
if ($url = http_request_get_common_syndication($values['source'])) {
$values['source'] = $url;
}
}
}
/**
* Override sourceSave() - subscribe to hub.
*/
public function sourceSave(FeedsSource $source) {
if ($this->config['use_pubsubhubbub']) {
// If this is a feeds node we want to delay the subscription to
// feeds_exit() to avoid transaction race conditions.
if ($source->feed_nid) {
$job = array('fetcher' => $this, 'source' => $source);
feeds_set_subscription_job($job);
}
else {
$this->subscribe($source);
}
}
}
/**
* Override sourceDelete() - unsubscribe from hub.
*/
public function sourceDelete(FeedsSource $source) {
if ($this->config['use_pubsubhubbub']) {
// If we're in a feed node, queue the unsubscribe,
// else process immediately.
if ($source->feed_nid) {
$job = array(
'type' => $source->id,
'id' => $source->feed_nid,
'period' => 0,
'periodic' => FALSE,
);
JobScheduler::get('feeds_push_unsubscribe')->set($job);
}
else {
$this->unsubscribe($source);
}
}
}
/**
* Implement FeedsFetcher::subscribe() - subscribe to hub.
*/
public function subscribe(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
$this->subscriber($source->feed_nid)->subscribe($source_config['source'], url($this->path($source->feed_nid), array('absolute' => TRUE)), valid_url($this->config['designated_hub']) ? $this->config['designated_hub'] : '');
}
/**
* Implement FeedsFetcher::unsubscribe() - unsubscribe from hub.
*/
public function unsubscribe(FeedsSource $source) {
$source_config = $source->getConfigFor($this);
$this->subscriber($source->feed_nid)->unsubscribe($source_config['source'], url($this->path($source->feed_nid), array('absolute' => TRUE)));
}
/**
* Implement FeedsFetcher::importPeriod().
*/
public function importPeriod(FeedsSource $source) {
if ($this->subscriber($source->feed_nid)->subscribed()) {
return 259200; // Delay for three days if there is a successful subscription.
}
}
/**
* Convenience method for instantiating a subscriber object.
*/
protected function subscriber($subscriber_id) {
return PushSubscriber::instance($this->id, $subscriber_id, 'PuSHSubscription', PuSHEnvironment::instance());
}
}
/**
* Implement a PuSHSubscriptionInterface.
*/
class PuSHSubscription implements PuSHSubscriptionInterface {
public $domain;
public $subscriber_id;
public $hub;
public $topic;
public $status;
public $secret;
public $post_fields;
public $timestamp;
/**
* Load a subscription.
*/
public static function load($domain, $subscriber_id) {
if ($v = db_query("SELECT * FROM {feeds_push_subscriptions} WHERE domain = :domain AND subscriber_id = :sid", array(':domain' => $domain, ':sid' => $subscriber_id))->fetchAssoc()) {
$v['post_fields'] = unserialize($v['post_fields']);
return new PuSHSubscription($v['domain'], $v['subscriber_id'], $v['hub'], $v['topic'], $v['secret'], $v['status'], $v['post_fields'], $v['timestamp']);
}
}
/**
* Create a subscription.
*/
public function __construct($domain, $subscriber_id, $hub, $topic, $secret, $status = '', $post_fields = '') {
$this->domain = $domain;
$this->subscriber_id = $subscriber_id;
$this->hub = $hub;
$this->topic = $topic;
$this->status = $status;
$this->secret = $secret;
$this->post_fields = $post_fields;
}
/**
* Save a subscription.
*/
public function save() {
$this->timestamp = time();
$this->delete($this->domain, $this->subscriber_id);
drupal_write_record('feeds_push_subscriptions', $this);
}
/**
* Delete a subscription.
*/
public function delete() {
db_delete('feeds_push_subscriptions')
->condition('domain', $this->domain)
->condition('subscriber_id', $this->subscriber_id)
->execute();
}
}
/**
* Provide environmental functions to the PuSHSubscriber library.
*/
class PuSHEnvironment implements PuSHSubscriberEnvironmentInterface {
/**
* Singleton.
*/
public static function instance() {
static $env;
if (empty($env)) {
$env = new PuSHEnvironment();
}
return $env;
}
/**
* Implements PuSHSubscriberEnvironmentInterface::msg().
*/
public function msg($msg, $level = 'status') {
drupal_set_message(check_plain($msg), $level);
}
/**
* Implements PuSHSubscriberEnvironmentInterface::log().
*/
public function log($msg, $level = 'status') {
switch ($level) {
case 'error':
$severity = WATCHDOG_ERROR;
break;
case 'warning':
$severity = WATCHDOG_WARNING;
break;
default:
$severity = WATCHDOG_NOTICE;
break;
}
feeds_dbg($msg);
watchdog('FeedsHTTPFetcher', $msg, array(), $severity);
}
}

View File

@@ -0,0 +1,391 @@
<?php
/**
* @file
* Class definition of FeedsNodeProcessor.
*/
/**
* Creates nodes from feed items.
*/
class FeedsNodeProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
public function entityType() {
return 'node';
}
/**
* Implements parent::entityInfo().
*/
protected function entityInfo() {
$info = parent::entityInfo();
$info['label plural'] = t('Nodes');
return $info;
}
/**
* Creates a new node in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$node = new stdClass();
$node->type = $this->config['content_type'];
$node->changed = REQUEST_TIME;
$node->created = REQUEST_TIME;
$node->language = LANGUAGE_NONE;
node_object_prepare($node);
// Populate properties that are set by node_object_prepare().
$node->log = 'Created by FeedsNodeProcessor';
$node->uid = $this->config['author'];
return $node;
}
/**
* Loads an existing node.
*
* If the update existing method is not FEEDS_UPDATE_EXISTING, only the node
* table will be loaded, foregoing the node_load API for better performance.
*
* @todo Reevaluate the use of node_object_prepare().
*/
protected function entityLoad(FeedsSource $source, $nid) {
if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
$node = node_load($nid, NULL, TRUE);
}
else {
// We're replacing the existing node. Only save the absolutely necessary.
$node = db_query("SELECT created, nid, vid, type, status FROM {node} WHERE nid = :nid", array(':nid' => $nid))->fetchObject();
$node->uid = $this->config['author'];
}
node_object_prepare($node);
// Workaround for issue #1247506. See #1245094 for backstory.
if (!empty($node->menu)) {
// If the node has a menu item(with a valid mlid) it must be flagged
// 'enabled'.
$node->menu['enabled'] = (int) (bool) $node->menu['mlid'];
}
// Populate properties that are set by node_object_prepare().
if ($this->config['update_existing'] == FEEDS_UPDATE_EXISTING) {
$node->log = 'Updated by FeedsNodeProcessor';
}
else {
$node->log = 'Replaced by FeedsNodeProcessor';
}
return $node;
}
/**
* Check that the user has permission to save a node.
*/
protected function entitySaveAccess($entity) {
// The check will be skipped for anonymous nodes.
if ($this->config['authorize'] && !empty($entity->uid)) {
$author = user_load($entity->uid);
// If the uid was mapped directly, rather than by email or username, it
// could be invalid.
if (!$author) {
$message = 'User %uid is not a valid user.';
throw new FeedsAccessException(t($message, array('%uid' => $entity->uid)));
}
if (empty($entity->nid) || !empty($entity->is_new)) {
$op = 'create';
$access = node_access($op, $entity->type, $author);
}
else {
$op = 'update';
$access = node_access($op, $entity, $author);
}
if (!$access) {
$message = 'User %name is not authorized to %op content type %content_type.';
throw new FeedsAccessException(t($message, array('%name' => $author->name, '%op' => $op, '%content_type' => $entity->type)));
}
}
}
/**
* Save a node.
*/
public function entitySave($entity) {
// If nid is set and a node with that id doesn't exist, flag as new.
if (!empty($entity->nid) && !node_load($entity->nid)) {
$entity->is_new = TRUE;
}
node_save($entity);
}
/**
* Delete a series of nodes.
*/
protected function entityDeleteMultiple($nids) {
node_delete_multiple($nids);
}
/**
* Implement expire().
*
* @todo: move to processor stage?
*/
public function expire($time = NULL) {
if ($time === NULL) {
$time = $this->expiryTime();
}
if ($time == FEEDS_EXPIRE_NEVER) {
return;
}
$count = $this->getLimit();
$nodes = db_query_range("SELECT n.nid FROM {node} n JOIN {feeds_item} fi ON fi.entity_type = 'node' AND n.nid = fi.entity_id WHERE fi.id = :id AND n.created < :created", 0, $count, array(':id' => $this->id, ':created' => REQUEST_TIME - $time));
$nids = array();
foreach ($nodes as $node) {
$nids[$node->nid] = $node->nid;
}
$this->entityDeleteMultiple($nids);
if (db_query_range("SELECT 1 FROM {node} n JOIN {feeds_item} fi ON fi.entity_type = 'node' AND n.nid = fi.entity_id WHERE fi.id = :id AND n.created < :created", 0, 1, array(':id' => $this->id, ':created' => REQUEST_TIME - $time))->fetchField()) {
return FEEDS_BATCH_ACTIVE;
}
return FEEDS_BATCH_COMPLETE;
}
/**
* Return expiry time.
*/
public function expiryTime() {
return $this->config['expire'];
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
$types = node_type_get_names();
$type = isset($types['article']) ? 'article' : key($types);
return array(
'content_type' => $type,
'expire' => FEEDS_EXPIRE_NEVER,
'author' => 0,
'authorize' => TRUE,
) + parent::configDefaults();
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$types = node_type_get_names();
array_walk($types, 'check_plain');
$form = parent::configForm($form_state);
$form['content_type'] = array(
'#type' => 'select',
'#title' => t('Content type'),
'#description' => t('Select the content type for the nodes to be created. <strong>Note:</strong> Users with "import !feed_id feeds" permissions will be able to <strong>import</strong> nodes of the content type selected here regardless of the node level permissions. Further, users with "clear !feed_id permissions" will be able to <strong>delete</strong> imported nodes regardless of their node level permissions.', array('!feed_id' => $this->id)),
'#options' => $types,
'#default_value' => $this->config['content_type'],
);
$author = user_load($this->config['author']);
$form['author'] = array(
'#type' => 'textfield',
'#title' => t('Author'),
'#description' => t('Select the author of the nodes to be created - leave empty to assign "anonymous".'),
'#autocomplete_path' => 'user/autocomplete',
'#default_value' => empty($author->name) ? 'anonymous' : check_plain($author->name),
);
$form['authorize'] = array(
'#type' => 'checkbox',
'#title' => t('Authorize'),
'#description' => t('Check that the author has permission to create the node.'),
'#default_value' => $this->config['authorize'],
);
$period = drupal_map_assoc(array(FEEDS_EXPIRE_NEVER, 3600, 10800, 21600, 43200, 86400, 259200, 604800, 2592000, 2592000 * 3, 2592000 * 6, 31536000), 'feeds_format_expire');
$form['expire'] = array(
'#type' => 'select',
'#title' => t('Expire nodes'),
'#options' => $period,
'#description' => t('Select after how much time nodes should be deleted. The node\'s published date will be used for determining the node\'s age, see Mapping settings.'),
'#default_value' => $this->config['expire'],
);
$form['update_existing']['#options'] = array(
FEEDS_SKIP_EXISTING => 'Do not update existing nodes',
FEEDS_REPLACE_EXISTING => 'Replace existing nodes',
FEEDS_UPDATE_EXISTING => 'Update existing nodes (slower than replacing them)',
);
return $form;
}
/**
* Override parent::configFormValidate().
*/
public function configFormValidate(&$values) {
if ($author = user_load_by_name($values['author'])) {
$values['author'] = $author->uid;
}
else {
$values['author'] = 0;
}
}
/**
* Reschedule if expiry time changes.
*/
public function configFormSubmit(&$values) {
if ($this->config['expire'] != $values['expire']) {
feeds_reschedule($this->id);
}
parent::configFormSubmit($values);
}
/**
* Override setTargetElement to operate on a target item that is a node.
*/
public function setTargetElement(FeedsSource $source, $target_node, $target_element, $value) {
switch ($target_element) {
case 'created':
$target_node->created = feeds_to_unixtime($value, REQUEST_TIME);
break;
case 'feeds_source':
// Get the class of the feed node importer's fetcher and set the source
// property. See feeds_node_update() how $node->feeds gets stored.
if ($id = feeds_get_importer_id($this->config['content_type'])) {
$class = get_class(feeds_importer($id)->fetcher);
$target_node->feeds[$class]['source'] = $value;
// This effectively suppresses 'import on submission' feature.
// See feeds_node_insert().
$target_node->feeds['suppress_import'] = TRUE;
}
break;
case 'user_name':
if ($user = user_load_by_name($value)) {
$target_node->uid = $user->uid;
}
break;
case 'user_mail':
if ($user = user_load_by_mail($value)) {
$target_node->uid = $user->uid;
}
break;
default:
parent::setTargetElement($source, $target_node, $target_element, $value);
break;
}
}
/**
* Return available mapping targets.
*/
public function getMappingTargets() {
$type = node_type_get_type($this->config['content_type']);
$targets = parent::getMappingTargets();
if ($type->has_title) {
$targets['title'] = array(
'name' => t('Title'),
'description' => t('The title of the node.'),
'optional_unique' => TRUE,
);
}
$targets['nid'] = array(
'name' => t('Node ID'),
'description' => t('The nid of the node. NOTE: use this feature with care, node ids are usually assigned by Drupal.'),
'optional_unique' => TRUE,
);
$targets['uid'] = array(
'name' => t('User ID'),
'description' => t('The Drupal user ID of the node author.'),
);
$targets['user_name'] = array(
'name' => t('Username'),
'description' => t('The Drupal username of the node author.'),
);
$targets['user_mail'] = array(
'name' => t('User email'),
'description' => t('The email address of the node author.'),
);
$targets['status'] = array(
'name' => t('Published status'),
'description' => t('Whether a node is published or not. 1 stands for published, 0 for not published.'),
);
$targets['created'] = array(
'name' => t('Published date'),
'description' => t('The UNIX time when a node has been published.'),
);
$targets['promote'] = array(
'name' => t('Promoted to front page'),
'description' => t('Boolean value, whether or not node is promoted to front page. (1 = promoted, 0 = not promoted)'),
);
$targets['sticky'] = array(
'name' => t('Sticky'),
'description' => t('Boolean value, whether or not node is sticky at top of lists. (1 = sticky, 0 = not sticky)'),
);
// Include language field if Locale module is enabled.
if (module_exists('locale')) {
$targets['language'] = array(
'name' => t('Language'),
'description' => t('The two-character language code of the node.'),
);
}
// Include comment field if Comment module is enabled.
if (module_exists('comment')) {
$targets['comment'] = array(
'name' => t('Comments'),
'description' => t('Whether comments are allowed on this node: 0 = no, 1 = read only, 2 = read/write.'),
);
}
// If the target content type is a Feed node, expose its source field.
if ($id = feeds_get_importer_id($this->config['content_type'])) {
$name = feeds_importer($id)->config['name'];
$targets['feeds_source'] = array(
'name' => t('Feed source'),
'description' => t('The content type created by this processor is a Feed Node, it represents a source itself. Depending on the fetcher selected on the importer "@importer", this field is expected to be for example a URL or a path to a file.', array('@importer' => $name)),
'optional_unique' => TRUE,
);
}
// Let other modules expose mapping targets.
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->config['content_type'];
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
return $targets;
}
/**
* Get nid of an existing feed item node if available.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
if ($nid = parent::existingEntityId($source, $result)) {
return $nid;
}
// Iterate through all unique targets and test whether they do already
// exist in the database.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
switch ($target) {
case 'nid':
$nid = db_query("SELECT nid FROM {node} WHERE nid = :nid", array(':nid' => $value))->fetchField();
break;
case 'title':
$nid = db_query("SELECT nid FROM {node} WHERE title = :title AND type = :type", array(':title' => $value, ':type' => $this->config['content_type']))->fetchField();
break;
case 'feeds_source':
if ($id = feeds_get_importer_id($this->config['content_type'])) {
$nid = db_query("SELECT fs.feed_nid FROM {node} n JOIN {feeds_source} fs ON n.nid = fs.feed_nid WHERE fs.id = :id AND fs.source = :source", array(':id' => $id, ':source' => $value))->fetchField();
}
break;
}
if ($nid) {
// Return with the first nid found.
return $nid;
}
}
return 0;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* @file
* OPML Parser plugin.
*/
/**
* Feeds parser plugin that parses OPML feeds.
*/
class FeedsOPMLParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
feeds_include_library('opml_parser.inc', 'opml_parser');
$opml = opml_parser_parse($fetcher_result->getRaw());
$result = new FeedsParserResult($opml['items']);
$result->title = $opml['title'];
return $result;
}
/**
* Return mapping sources.
*/
public function getMappingSources() {
return array(
'title' => array(
'name' => t('Feed title'),
'description' => t('Title of the feed.'),
),
'xmlurl' => array(
'name' => t('Feed URL'),
'description' => t('URL of the feed.'),
),
) + parent::getMappingSources();
}
}

View File

@@ -0,0 +1,757 @@
<?php
/**
* @file
* Contains FeedsParser and related classes.
*/
/**
* A result of a parsing stage.
*/
class FeedsParserResult extends FeedsResult {
public $title;
public $description;
public $link;
public $items;
public $current_item;
/**
* Constructor.
*/
public function __construct($items = array()) {
$this->title = '';
$this->description = '';
$this->link = '';
$this->items = $items;
}
/**
* @todo Move to a nextItem() based approach, not consuming the item array.
* Can only be done once we don't cache the entire batch object between page
* loads for batching anymore.
*
* @return
* Next available item or NULL if there is none. Every returned item is
* removed from the internal array.
*/
public function shiftItem() {
$this->current_item = array_shift($this->items);
return $this->current_item;
}
/**
* @return
* Current result item.
*/
public function currentItem() {
return empty($this->current_item) ? NULL : $this->current_item;
}
}
/**
* Abstract class, defines interface for parsers.
*/
abstract class FeedsParser extends FeedsPlugin {
/**
* Parse content fetched by fetcher.
*
* Extending classes must implement this method.
*
* @param FeedsSource $source
* Source information.
* @param $fetcher_result
* FeedsFetcherResult returned by fetcher.
*/
public abstract function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result);
/**
* Clear all caches for results for given source.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers can choose to only clear
* caches pertaining to this source.
*/
public function clear(FeedsSource $source) {}
/**
* Declare the possible mapping sources that this parser produces.
*
* @ingroup mappingapi
*
* @return
* An array of mapping sources, or FALSE if the sources can be defined by
* typing a value in a text field.
*
* Example:
* @code
* array(
* 'title' => t('Title'),
* 'created' => t('Published date'),
* 'url' => t('Feed item URL'),
* 'guid' => t('Feed item GUID'),
* )
* @endcode
*/
public function getMappingSources() {
self::loadMappers();
$sources = array();
$content_type = feeds_importer($this->id)->config['content_type'];
drupal_alter('feeds_parser_sources', $sources, $content_type);
if (!feeds_importer($this->id)->config['content_type']) {
return $sources;
}
$sources['parent:uid'] = array(
'name' => t('Feed node: User ID'),
'description' => t('The feed node author uid.'),
);
$sources['parent:nid'] = array(
'name' => t('Feed node: Node ID'),
'description' => t('The feed node nid.'),
);
return $sources;
}
/**
* Get an element identified by $element_key of the given item.
* The element key corresponds to the values in the array returned by
* FeedsParser::getMappingSources().
*
* This method is invoked from FeedsProcessor::map() when a concrete item is
* processed.
*
* @ingroup mappingapi
*
* @param $batch
* FeedsImportBatch object containing the sources to be mapped from.
* @param $element_key
* The key identifying the element that should be retrieved from $source
*
* @return
* The source element from $item identified by $element_key.
*
* @see FeedsProcessor::map()
* @see FeedsCSVParser::getSourceElement()
*/
public function getSourceElement(FeedsSource $source, FeedsParserResult $result, $element_key) {
switch ($element_key) {
case 'parent:uid':
if ($source->feed_nid && $node = node_load($source->feed_nid)) {
return $node->uid;
}
break;
case 'parent:nid':
return $source->feed_nid;
}
$item = $result->currentItem();
return isset($item[$element_key]) ? $item[$element_key] : '';
}
}
/**
* Defines an element of a parsed result. Such an element can be a simple type,
* a complex type (derived from FeedsElement) or an array of either.
*
* @see FeedsEnclosure
*/
class FeedsElement {
// The standard value of this element. This value can contain be a simple type,
// a FeedsElement or an array of either.
protected $value;
/**
* Constructor.
*/
public function __construct($value) {
$this->value = $value;
}
/**
* @todo Make value public and deprecate use of getValue().
*
* @return
* Value of this FeedsElement represented as a scalar.
*/
public function getValue() {
return $this->value;
}
/**
* Magic method __toString() for printing and string conversion of this
* object.
*
* @return
* A string representation of this element.
*/
public function __toString() {
if (is_array($this->value)) {
return 'Array';
}
if (is_object($this->value)) {
return 'Object';
}
return (string) $this->getValue();
}
}
/**
* Encapsulates a taxonomy style term object.
*
* Objects of this class can be turned into a taxonomy term style arrays by
* casting them.
*
* @code
* $term_object = new FeedsTermElement($term_array);
* $term_array = (array)$term_object;
* @endcode
*/
class FeedsTermElement extends FeedsElement {
public $tid, $vid, $name;
/**
* @param $term
* An array or a stdClass object that is a Drupal taxonomy term.
*/
public function __construct($term) {
if (is_array($term)) {
parent::__construct($term['name']);
foreach ($this as $key => $value) {
$this->$key = isset($term[$key]) ? $term[$key] : NULL;
}
}
elseif (is_object($term)) {
parent::__construct($term->name);
foreach ($this as $key => $value) {
$this->$key = isset($term->$key) ? $term->$key : NULL;
}
}
}
/**
* Use $name as $value.
*/
public function getValue() {
return $this->name;
}
}
/**
* A geo term element.
*/
class FeedsGeoTermElement extends FeedsTermElement {
public $lat, $lon, $bound_top, $bound_right, $bound_bottom, $bound_left, $geometry;
/**
* @param $term
* An array or a stdClass object that is a Drupal taxonomy term. Can include
* geo extensions.
*/
public function __construct($term) {
parent::__construct($term);
}
}
/**
* Enclosure element, can be part of the result array.
*/
class FeedsEnclosure extends FeedsElement {
protected $mime_type;
/**
* Constructor, requires MIME type.
*
* @param $value
* A path to a local file or a URL to a remote document.
* @param $mimetype
* The mime type of the resource.
*/
public function __construct($value, $mime_type) {
parent::__construct($value);
$this->mime_type = $mime_type;
}
/**
* @return
* MIME type of return value of getValue().
*/
public function getMIMEType() {
return $this->mime_type;
}
/**
* Use this method instead of FeedsElement::getValue() when fetching the file
* from the URL.
*
* @return
* Value with encoded space characters to safely fetch the file from the URL.
*
* @see FeedsElement::getValue()
*/
public function getUrlEncodedValue() {
return str_replace(' ', '%20', $this->getValue());
}
/**
* Use this method instead of FeedsElement::getValue() to get the file name
* transformed for better local saving (underscores instead of spaces)
*
* @return
* Value with space characters changed to underscores.
*
* @see FeedsElement::getValue()
*/
public function getLocalValue() {
return str_replace(' ', '_', $this->getValue());
}
/**
* @return
* The content of the referenced resource.
*/
public function getContent() {
feeds_include_library('http_request.inc', 'http_request');
$result = http_request_get($this->getUrlEncodedValue());
if ($result->code != 200) {
throw new Exception(t('Download of @url failed with code !code.', array('@url' => $this->getUrlEncodedValue(), '!code' => $result->code)));
}
return $result->data;
}
/**
* Get a Drupal file object of the enclosed resource, download if necessary.
*
* @param $destination
* The path or uri specifying the target directory in which the file is
* expected. Don't use trailing slashes unless it's a streamwrapper scheme.
*
* @return
* A Drupal temporary file object of the enclosed resource.
*
* @throws Exception
* If file object could not be created.
*/
public function getFile($destination) {
if ($this->getValue()) {
// Prepare destination directory.
file_prepare_directory($destination, FILE_MODIFY_PERMISSIONS | FILE_CREATE_DIRECTORY);
// Copy or save file depending on whether it is remote or local.
if (drupal_realpath($this->getValue())) {
$file = new stdClass();
$file->uid = 0;
$file->uri = $this->getValue();
$file->filemime = $this->mime_type;
$file->filename = basename($file->uri);
if (dirname($file->uri) != $destination) {
$file = file_copy($file, $destination);
}
else {
// If file is not to be copied, check whether file already exists,
// as file_save() won't do that for us (compare file_copy() and
// file_save())
$existing_files = file_load_multiple(array(), array('uri' => $file->uri));
if (count($existing_files)) {
$existing = reset($existing_files);
$file->fid = $existing->fid;
$file->filename = $existing->filename;
}
file_save($file);
}
}
else {
$filename = basename($this->getLocalValue());
if (module_exists('transliteration')) {
require_once drupal_get_path('module', 'transliteration') . '/transliteration.inc';
$filename = transliteration_clean_filename($filename);
}
if (file_uri_target($destination)) {
$destination = trim($destination, '/') . '/';
}
try {
$file = file_save_data($this->getContent(), $destination . $filename);
}
catch (Exception $e) {
watchdog_exception('Feeds', $e, nl2br(check_plain($e)));
}
}
// We couldn't make sense of this enclosure, throw an exception.
if (!$file) {
throw new Exception(t('Invalid enclosure %enclosure', array('%enclosure' => $this->getValue())));
}
}
return $file;
}
}
/**
* Defines a date element of a parsed result (including ranges, repeat).
*/
class FeedsDateTimeElement extends FeedsElement {
// Start date and end date.
public $start;
public $end;
/**
* Constructor.
*
* @param $start
* A FeedsDateTime object or a date as accepted by FeedsDateTime.
* @param $end
* A FeedsDateTime object or a date as accepted by FeedsDateTime.
* @param $tz
* A PHP DateTimeZone object.
*/
public function __construct($start = NULL, $end = NULL, $tz = NULL) {
$this->start = (!isset($start) || ($start instanceof FeedsDateTime)) ? $start : new FeedsDateTime($start, $tz);
$this->end = (!isset($end) || ($end instanceof FeedsDateTime)) ? $end : new FeedsDateTime($end, $tz);
}
/**
* Override FeedsElement::getValue().
*
* @return
* The UNIX timestamp of this object's start date. Return value is
* technically a string but will only contain numeric values.
*/
public function getValue() {
if ($this->start) {
return $this->start->format('U');
}
return '0';
}
/**
* Merge this field with another. Most stuff goes down when merging the two
* sub-dates.
*
* @see FeedsDateTime
*/
public function merge(FeedsDateTimeElement $other) {
$this2 = clone $this;
if ($this->start && $other->start) {
$this2->start = $this->start->merge($other->start);
}
elseif ($other->start) {
$this2->start = clone $other->start;
}
elseif ($this->start) {
$this2->start = clone $this->start;
}
if ($this->end && $other->end) {
$this2->end = $this->end->merge($other->end);
}
elseif ($other->end) {
$this2->end = clone $other->end;
}
elseif ($this->end) {
$this2->end = clone $this->end;
}
return $this2;
}
/**
* Helper method for buildDateField(). Build a FeedsDateTimeElement object
* from a standard formatted node.
*/
protected static function readDateField($entity, $field_name) {
$ret = new FeedsDateTimeElement();
if (isset($entity->{$field_name}['und'][0]['date']) && $entity->{$field_name}['und'][0]['date'] instanceof FeedsDateTime) {
$ret->start = $entity->{$field_name}['und'][0]['date'];
}
if (isset($entity->{$field_name}['und'][0]['date2']) && $entity->{$field_name}['und'][0]['date2'] instanceof FeedsDateTime) {
$ret->end = $entity->{$field_name}['und'][0]['date2'];
}
return $ret;
}
/**
* Build a entity's date field from our object.
*
* @param $entity
* The entity to build the date field on.
* @param $field_name
* The name of the field to build.
*/
public function buildDateField($entity, $field_name) {
$info = field_info_field($field_name);
$oldfield = FeedsDateTimeElement::readDateField($entity, $field_name);
// Merge with any preexisting objects on the field; we take precedence.
$oldfield = $this->merge($oldfield);
$use_start = $oldfield->start;
$use_end = $oldfield->end;
// Set timezone if not already in the FeedsDateTime object
$to_tz = date_get_timezone($info['settings']['tz_handling'], date_default_timezone());
$temp = new FeedsDateTime(NULL, new DateTimeZone($to_tz));
$db_tz = '';
if ($use_start) {
$use_start = $use_start->merge($temp);
if (!date_timezone_is_valid($use_start->getTimezone()->getName())) {
$use_start->setTimezone(new DateTimeZone("UTC"));
}
$db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_start->getTimezone()->getName());
}
if ($use_end) {
$use_end = $use_end->merge($temp);
if (!date_timezone_is_valid($use_end->getTimezone()->getName())) {
$use_end->setTimezone(new DateTimeZone("UTC"));
}
if (!$db_tz) {
$db_tz = date_get_timezone_db($info['settings']['tz_handling'], $use_end->getTimezone()->getName());
}
}
if (!$db_tz) {
return;
}
$db_tz = new DateTimeZone($db_tz);
if (!isset($entity->{$field_name})) {
$entity->{$field_name} = array('und' => array());
}
if ($use_start) {
$entity->{$field_name}['und'][0]['timezone'] = $use_start->getTimezone()->getName();
$entity->{$field_name}['und'][0]['offset'] = $use_start->getOffset();
$use_start->setTimezone($db_tz);
$entity->{$field_name}['und'][0]['date'] = $use_start;
/**
* @todo the date_type_format line could be simplified based upon a patch
* DO issue #259308 could affect this, follow up on at some point.
* Without this, all granularity info is lost.
* $use_start->format(date_type_format($field['type'], $use_start->granularity));
*/
$entity->{$field_name}['und'][0]['value'] = $use_start->format(date_type_format($info['type']));
}
if ($use_end) {
// Don't ever use end to set timezone (for now)
$entity->{$field_name}['und'][0]['offset2'] = $use_end->getOffset();
$use_end->setTimezone($db_tz);
$entity->{$field_name}['und'][0]['date2'] = $use_end;
$entity->{$field_name}['und'][0]['value2'] = $use_end->format(date_type_format($info['type']));
}
}
}
/**
* Extend PHP DateTime class with granularity handling, merge functionality and
* slightly more flexible initialization parameters.
*
* This class is a Drupal independent extension of the >= PHP 5.2 DateTime
* class.
*
* @see FeedsDateTimeElement
*/
class FeedsDateTime extends DateTime {
public $granularity = array();
protected static $allgranularity = array('year', 'month', 'day', 'hour', 'minute', 'second', 'zone');
private $_serialized_time;
private $_serialized_timezone;
/**
* Helper function to prepare the object during serialization.
*
* We are extending a core class and core classes cannot be serialized.
*
* Ref: http://bugs.php.net/41334, http://bugs.php.net/39821
*/
public function __sleep() {
$this->_serialized_time = $this->format('c');
$this->_serialized_timezone = $this->getTimezone()->getName();
return array('_serialized_time', '_serialized_timezone');
}
/**
* Upon unserializing, we must re-build ourselves using local variables.
*/
public function __wakeup() {
$this->__construct($this->_serialized_time, new DateTimeZone($this->_serialized_timezone));
}
/**
* Overridden constructor.
*
* @param $time
* time string, flexible format including timestamp. Invalid formats will
* fall back to 'now'.
* @param $tz
* PHP DateTimeZone object, NULL allowed
*/
public function __construct($time = '', $tz = NULL) {
// Assume UNIX timestamp if numeric.
if (is_numeric($time)) {
// Make sure it's not a simple year
if ((is_string($time) && strlen($time) > 4) || is_int($time)) {
$time = "@" . $time;
}
}
// PHP < 5.3 doesn't like the GMT- notation for parsing timezones.
$time = str_replace("GMT-", "-", $time);
$time = str_replace("GMT+", "+", $time);
// Some PHP 5.2 version's DateTime class chokes on invalid dates.
if (!strtotime($time)) {
$time = 'now';
}
// Create and set time zone separately, PHP 5.2.6 does not respect time zone
// argument in __construct().
parent::__construct($time);
$tz = $tz ? $tz : new DateTimeZone("UTC");
$this->setTimeZone($tz);
// Verify that timezone has not been specified as an offset.
if (!preg_match('/[a-zA-Z]/', $this->getTimezone()->getName())) {
$this->setTimezone(new DateTimeZone("UTC"));
}
// Finally set granularity.
$this->setGranularityFromTime($time, $tz);
}
/**
* This function will keep this object's values by default.
*/
public function merge(FeedsDateTime $other) {
$other_tz = $other->getTimezone();
$this_tz = $this->getTimezone();
// Figure out which timezone to use for combination.
$use_tz = ($this->hasGranularity('zone') || !$other->hasGranularity('zone')) ? $this_tz : $other_tz;
$this2 = clone $this;
$this2->setTimezone($use_tz);
$other->setTimezone($use_tz);
$val = $this2->toArray();
$otherval = $other->toArray();
foreach (self::$allgranularity as $g) {
if ($other->hasGranularity($g) && !$this2->hasGranularity($g)) {
// The other class has a property we don't; steal it.
$this2->addGranularity($g);
$val[$g] = $otherval[$g];
}
}
$other->setTimezone($other_tz);
$this2->setDate($val['year'], $val['month'], $val['day']);
$this2->setTime($val['hour'], $val['minute'], $val['second']);
return $this2;
}
/**
* Overrides default DateTime function. Only changes output values if
* actually had time granularity. This should be used as a "converter" for
* output, to switch tzs.
*
* In order to set a timezone for a datetime that doesn't have such
* granularity, merge() it with one that does.
*/
public function setTimezone($tz, $force = FALSE) {
// PHP 5.2.6 has a fatal error when setting a date's timezone to itself.
// http://bugs.php.net/bug.php?id=45038
if (version_compare(PHP_VERSION, '5.2.7', '<') && $tz == $this->getTimezone()) {
$tz = new DateTimeZone($tz->getName());
}
if (!$this->hasTime() || !$this->hasGranularity('zone') || $force) {
// this has no time or timezone granularity, so timezone doesn't mean much
// We set the timezone using the method, which will change the day/hour, but then we switch back
$arr = $this->toArray();
parent::setTimezone($tz);
$this->setDate($arr['year'], $arr['month'], $arr['day']);
$this->setTime($arr['hour'], $arr['minute'], $arr['second']);
return;
}
parent::setTimezone($tz);
}
/**
* Safely adds a granularity entry to the array.
*/
public function addGranularity($g) {
$this->granularity[] = $g;
$this->granularity = array_unique($this->granularity);
}
/**
* Removes a granularity entry from the array.
*/
public function removeGranularity($g) {
if ($key = array_search($g, $this->granularity)) {
unset($this->granularity[$key]);
}
}
/**
* Checks granularity array for a given entry.
*/
public function hasGranularity($g) {
return in_array($g, $this->granularity);
}
/**
* Returns whether this object has time set. Used primarily for timezone
* conversion and fomratting.
*
* @todo currently very simplistic, but effective, see usage
*/
public function hasTime() {
return $this->hasGranularity('hour');
}
/**
* Protected function to find the granularity given by the arguments to the
* constructor.
*/
protected function setGranularityFromTime($time, $tz) {
$this->granularity = array();
$temp = date_parse($time);
// This PHP method currently doesn't have resolution down to seconds, so if
// there is some time, all will be set.
foreach (self::$allgranularity AS $g) {
if ((isset($temp[$g]) && is_numeric($temp[$g])) || ($g == 'zone' && (isset($temp['zone_type']) && $temp['zone_type'] > 0))) {
$this->granularity[] = $g;
}
}
if ($tz) {
$this->addGranularity('zone');
}
}
/**
* Helper to return all standard date parts in an array.
*/
protected function toArray() {
return array('year' => $this->format('Y'), 'month' => $this->format('m'), 'day' => $this->format('d'), 'hour' => $this->format('H'), 'minute' => $this->format('i'), 'second' => $this->format('s'), 'zone' => $this->format('e'));
}
}
/**
* Converts to UNIX time.
*
* @param $date
* A date that is either a string, a FeedsDateTimeElement or a UNIX timestamp.
* @param $default_value
* A default UNIX timestamp to return if $date could not be parsed.
*
* @return
* $date as UNIX time if conversion was successful, $dfeault_value otherwise.
*/
function feeds_to_unixtime($date, $default_value) {
if (is_numeric($date)) {
return $date;
}
elseif (is_string($date) && !empty($date)) {
$date = new FeedsDateTimeElement($date);
return $date->getValue();
}
elseif ($date instanceof FeedsDateTimeElement) {
return $date->getValue();
}
return $default_value;
}

View File

@@ -0,0 +1,216 @@
<?php
/**
* @file
* Definition of FeedsPlugin class.
*/
/**
* Base class for a fetcher, parser or processor result.
*/
class FeedsResult {}
/**
* Implement source interface for all plugins.
*
* Note how this class does not attempt to store source information locally.
* Doing this would break the model where source information is represented by
* an object that is being passed into a Feed object and its plugins.
*/
abstract class FeedsPlugin extends FeedsConfigurable implements FeedsSourceInterface {
/**
* Constructor.
*
* Initialize class variables.
*/
protected function __construct($id) {
parent::__construct($id);
$this->source_config = $this->sourceDefaults();
}
/**
* Save changes to the configuration of this object.
* Delegate saving to parent (= Feed) which will collect
* information from this object by way of getConfig() and store it.
*/
public function save() {
feeds_importer($this->id)->save();
}
/**
* Returns TRUE if $this->sourceForm() returns a form.
*/
public function hasSourceConfig() {
$form = $this->sourceForm(array());
return !empty($form);
}
/**
* Implements FeedsSourceInterface::sourceDefaults().
*/
public function sourceDefaults() {
$values = array_flip(array_keys($this->sourceForm(array())));
foreach ($values as $k => $v) {
$values[$k] = '';
}
return $values;
}
/**
* Callback methods, exposes source form.
*/
public function sourceForm($source_config) {
return array();
}
/**
* Validation handler for sourceForm.
*/
public function sourceFormValidate(&$source_config) {}
/**
* A source is being saved.
*/
public function sourceSave(FeedsSource $source) {}
/**
* A source is being deleted.
*/
public function sourceDelete(FeedsSource $source) {}
/**
* Loads on-behalf implementations from mappers/ directory.
*
* FeedsProcessor::map() does not load from mappers/ as only node and user
* processor ship with on-behalf implementations.
*
* @see FeedsNodeProcessor::map()
* @see FeedsUserProcessor::map()
*
* @todo: Use CTools Plugin API.
*/
protected static function loadMappers() {
static $loaded = FALSE;
if (!$loaded) {
$path = drupal_get_path('module', 'feeds') . '/mappers';
$files = drupal_system_listing('/.*\.inc$/', $path, 'name', 0);
foreach ($files as $file) {
if (strstr($file->uri, '/mappers/')) {
require_once(DRUPAL_ROOT . '/' . $file->uri);
}
}
}
$loaded = TRUE;
}
/**
* Get all available plugins.
*/
public static function all() {
ctools_include('plugins');
$plugins = ctools_get_plugins('feeds', 'plugins');
$result = array();
foreach ($plugins as $key => $info) {
if (!empty($info['hidden'])) {
continue;
}
$result[$key] = $info;
}
// Sort plugins by name and return.
uasort($result, 'feeds_plugin_compare');
return $result;
}
/**
* Determines whether given plugin is derived from given base plugin.
*
* @param $plugin_key
* String that identifies a Feeds plugin key.
* @param $parent_plugin
* String that identifies a Feeds plugin key to be tested against.
*
* @return
* TRUE if $parent_plugin is directly *or indirectly* a parent of $plugin,
* FALSE otherwise.
*/
public static function child($plugin_key, $parent_plugin) {
ctools_include('plugins');
$plugins = ctools_get_plugins('feeds', 'plugins');
$info = $plugins[$plugin_key];
if (empty($info['handler']['parent'])) {
return FALSE;
}
elseif ($info['handler']['parent'] == $parent_plugin) {
return TRUE;
}
else {
return self::child($info['handler']['parent'], $parent_plugin);
}
}
/**
* Determines the type of a plugin.
*
* @todo PHP5.3: Implement self::type() and query with $plugin_key::type().
*
* @param $plugin_key
* String that identifies a Feeds plugin key.
*
* @return
* One of the following values:
* 'fetcher' if the plugin is a fetcher
* 'parser' if the plugin is a parser
* 'processor' if the plugin is a processor
* FALSE otherwise.
*/
public static function typeOf($plugin_key) {
if (self::child($plugin_key, 'FeedsFetcher')) {
return 'fetcher';
}
elseif (self::child($plugin_key, 'FeedsParser')) {
return 'parser';
}
elseif (self::child($plugin_key, 'FeedsProcessor')) {
return 'processor';
}
return FALSE;
}
/**
* Gets all available plugins of a particular type.
*
* @param $type
* 'fetcher', 'parser' or 'processor'
*/
public static function byType($type) {
$plugins = self::all();
$result = array();
foreach ($plugins as $key => $info) {
if ($type == self::typeOf($key)) {
$result[$key] = $info;
}
}
return $result;
}
}
/**
* Used when a plugin is missing.
*/
class FeedsMissingPlugin extends FeedsPlugin {
public function menuItem() {
return array();
}
}
/**
* Sort callback for FeedsPlugin::all().
*/
function feeds_plugin_compare($a, $b) {
return strcasecmp($a['name'], $b['name']);
}

View File

@@ -0,0 +1,705 @@
<?php
/**
* @file
* Contains FeedsProcessor and related classes.
*/
// Update mode for existing items.
define('FEEDS_SKIP_EXISTING', 0);
define('FEEDS_REPLACE_EXISTING', 1);
define('FEEDS_UPDATE_EXISTING', 2);
// Default limit for creating items on a page load, not respected by all
// processors.
define('FEEDS_PROCESS_LIMIT', 50);
/**
* Thrown if a validation fails.
*/
class FeedsValidationException extends Exception {}
/**
* Thrown if a an access check fails.
*/
class FeedsAccessException extends Exception {}
/**
* Abstract class, defines interface for processors.
*/
abstract class FeedsProcessor extends FeedsPlugin {
/**
* @defgroup entity_api_wrapper Entity API wrapper.
*/
/**
* Entity type this processor operates on.
*/
public abstract function entityType();
/**
* Create a new entity.
*
* @param $source
* The feeds source that spawns this entity.
*
* @return
* A new entity object.
*/
protected abstract function newEntity(FeedsSource $source);
/**
* Load an existing entity.
*
* @param $source
* The feeds source that spawns this entity.
* @param $entity_id
* The unique id of the entity that should be loaded.
*
* @return
* A new entity object.
*/
protected abstract function entityLoad(FeedsSource $source, $entity_id);
/**
* Validate an entity.
*
* @throws FeedsValidationException $e
* If validation fails.
*/
protected function entityValidate($entity) {}
/**
* Access check for saving an enity.
*
* @param $entity
* Entity to be saved.
*
* @throws FeedsAccessException $e
* If the access check fails.
*/
protected function entitySaveAccess($entity) {}
/**
* Save an entity.
*
* @param $entity
* Entity to be saved.
*/
protected abstract function entitySave($entity);
/**
* Delete a series of entities.
*
* @param $entity_ids
* Array of unique identity ids to be deleted.
*/
protected abstract function entityDeleteMultiple($entity_ids);
/**
* Wrap entity_get_info() into a method so that extending classes can override
* it and more entity information. Allowed additional keys:
*
* 'label plural' ... the plural label of an entity type.
*/
protected function entityInfo() {
return entity_get_info($this->entityType());
}
/**
* @}
*/
/**
* Process the result of the parsing stage.
*
* @param FeedsSource $source
* Source information about this import.
* @param FeedsParserResult $parser_result
* The result of the parsing stage.
*/
public function process(FeedsSource $source, FeedsParserResult $parser_result) {
$state = $source->state(FEEDS_PROCESS);
while ($item = $parser_result->shiftItem()) {
// Check if this item already exists.
$entity_id = $this->existingEntityId($source, $parser_result);
$skip_existing = $this->config['update_existing'] == FEEDS_SKIP_EXISTING;
// If it exists, and we are not updating, pass onto the next item.
if ($entity_id && $skip_existing) {
continue;
}
$hash = $this->hash($item);
$changed = ($hash !== $this->getHash($entity_id));
$force_update = $this->config['skip_hash_check'];
// Do not proceed if the item exists, has not changed, and we're not
// forcing the update.
if ($entity_id && !$changed && !$force_update) {
continue;
}
try {
// Build a new entity.
if (empty($entity_id)) {
$entity = $this->newEntity($source);
$this->newItemInfo($entity, $source->feed_nid, $hash);
}
// Load an existing entity.
else {
$entity = $this->entityLoad($source, $entity_id);
// The feeds_item table is always updated with the info for the most recently processed entity.
// The only carryover is the entity_id.
$this->newItemInfo($entity, $source->feed_nid, $hash);
$entity->feeds_item->entity_id = $entity_id;
}
// Set property and field values.
$this->map($source, $parser_result, $entity);
$this->entityValidate($entity);
// Allow modules to alter the entity before saving.
module_invoke_all('feeds_presave', $source, $entity, $item);
if (module_exists('rules')) {
rules_invoke_event('feeds_import_'. $source->importer()->id, $entity);
}
// Enable modules to skip saving at all.
if (!empty($entity->feeds_item->skip)) {
continue;
}
// This will throw an exception on failure.
$this->entitySaveAccess($entity);
$this->entitySave($entity);
// Track progress.
if (empty($entity_id)) {
$state->created++;
}
else {
$state->updated++;
}
}
// Something bad happened, log it.
catch (Exception $e) {
$state->failed++;
drupal_set_message($e->getMessage(), 'warning');
$message = $e->getMessage();
$message .= '<h3>Original item</h3>';
$message .= '<pre>' . var_export($item, TRUE) . '</pre>';
$message .= '<h3>Entity</h3>';
$message .= '<pre>' . var_export($entity, TRUE) . '</pre>';
$source->log('import', $message, array(), WATCHDOG_ERROR);
}
}
// Set messages if we're done.
if ($source->progressImporting() != FEEDS_BATCH_COMPLETE) {
return;
}
$info = $this->entityInfo();
$tokens = array(
'@entity' => strtolower($info['label']),
'@entities' => strtolower($info['label plural']),
);
$messages = array();
if ($state->created) {
$messages[] = array(
'message' => format_plural(
$state->created,
'Created @number @entity.',
'Created @number @entities.',
array('@number' => $state->created) + $tokens
),
);
}
if ($state->updated) {
$messages[] = array(
'message' => format_plural(
$state->updated,
'Updated @number @entity.',
'Updated @number @entities.',
array('@number' => $state->updated) + $tokens
),
);
}
if ($state->failed) {
$messages[] = array(
'message' => format_plural(
$state->failed,
'Failed importing @number @entity.',
'Failed importing @number @entities.',
array('@number' => $state->failed) + $tokens
),
'level' => WATCHDOG_ERROR,
);
}
if (empty($messages)) {
$messages[] = array(
'message' => t('There are no new @entities.', array('@entities' => strtolower($info['label plural']))),
);
}
foreach ($messages as $message) {
drupal_set_message($message['message']);
$source->log('import', $message['message'], array(), isset($message['level']) ? $message['level'] : WATCHDOG_INFO);
}
}
/**
* Remove all stored results or stored results up to a certain time for a
* source.
*
* @param FeedsSource $source
* Source information for this expiry. Implementers should only delete items
* pertaining to this source. The preferred way of determining whether an
* item pertains to a certain souce is by using $source->feed_nid. It is the
* processor's responsibility to store the feed_nid of an imported item in
* the processing stage.
*/
public function clear(FeedsSource $source) {
$state = $source->state(FEEDS_PROCESS_CLEAR);
// Build base select statement.
$info = $this->entityInfo();
$select = db_select($info['base table'], 'e');
$select->addField('e', $info['entity keys']['id'], 'entity_id');
$select->join(
'feeds_item',
'fi',
"e.{$info['entity keys']['id']} = fi.entity_id AND fi.entity_type = '{$this->entityType()}'");
$select->condition('fi.id', $this->id);
$select->condition('fi.feed_nid', $source->feed_nid);
// If there is no total, query it.
if (!$state->total) {
$state->total = $select->countQuery()
->execute()
->fetchField();
}
// Delete a batch of entities.
$entities = $select->range(0, $this->getLimit())->execute();
$entity_ids = array();
foreach ($entities as $entity) {
$entity_ids[$entity->entity_id] = $entity->entity_id;
}
$this->entityDeleteMultiple($entity_ids);
// Report progress, take into account that we may not have deleted as
// many items as we have counted at first.
if (count($entity_ids)) {
$state->deleted += count($entity_ids);
$state->progress($state->total, $state->deleted);
}
else {
$state->progress($state->total, $state->total);
}
// Report results when done.
if ($source->progressClearing() == FEEDS_BATCH_COMPLETE) {
if ($state->deleted) {
$message = format_plural(
$state->deleted,
'Deleted @number @entity',
'Deleted @number @entities',
array(
'@number' => $state->deleted,
'@entity' => strtolower($info['label']),
'@entities' => strtolower($info['label plural']),
)
);
$source->log('clear', $message, array(), WATCHDOG_INFO);
drupal_set_message($message);
}
else {
drupal_set_message(t('There are no @entities to be deleted.', array('@entities' => $info['label plural'])));
}
}
}
/*
* Report number of items that can be processed per call.
*
* 0 means 'unlimited'.
*
* If a number other than 0 is given, Feeds parsers that support batching
* will only deliver this limit to the processor.
*
* @see FeedsSource::getLimit()
* @see FeedsCSVParser::parse()
*/
public function getLimit() {
return variable_get('feeds_process_limit', FEEDS_PROCESS_LIMIT);
}
/**
* Delete feed items younger than now - $time. Do not invoke expire on a
* processor directly, but use FeedsImporter::expire() instead.
*
* @see FeedsImporter::expire().
* @see FeedsDataProcessor::expire().
*
* @param $time
* If implemented, all items produced by this configuration that are older
* than REQUEST_TIME - $time should be deleted.
* If $time === NULL processor should use internal configuration.
*
* @return
* FEEDS_BATCH_COMPLETE if all items have been processed, a float between 0
* and 0.99* indicating progress otherwise.
*/
public function expire($time = NULL) {
return FEEDS_BATCH_COMPLETE;
}
/**
* Counts the number of items imported by this processor.
*/
public function itemCount(FeedsSource $source) {
return db_query("SELECT count(*) FROM {feeds_item} WHERE id = :id AND entity_type = :entity_type AND feed_nid = :feed_nid", array(':id' => $this->id, ':entity_type' => $this->entityType(), ':feed_nid' => $source->feed_nid))->fetchField();
}
/**
* Execute mapping on an item.
*
* This method encapsulates the central mapping functionality. When an item is
* processed, it is passed through map() where the properties of $source_item
* are mapped onto $target_item following the processor's mapping
* configuration.
*
* For each mapping FeedsParser::getSourceElement() is executed to retrieve
* the source element, then FeedsProcessor::setTargetElement() is invoked
* to populate the target item properly. Alternatively a
* hook_x_targets_alter() may have specified a callback for a mapping target
* in which case the callback is asked to populate the target item instead of
* FeedsProcessor::setTargetElement().
*
* @ingroup mappingapi
*
* @see hook_feeds_parser_sources_alter()
* @see hook_feeds_data_processor_targets_alter()
* @see hook_feeds_node_processor_targets_alter()
* @see hook_feeds_term_processor_targets_alter()
* @see hook_feeds_user_processor_targets_alter()
*/
protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
// Static cache $targets as getMappingTargets() may be an expensive method.
static $sources;
if (!isset($sources[$this->id])) {
$sources[$this->id] = feeds_importer($this->id)->parser->getMappingSources();
}
static $targets;
if (!isset($targets[$this->id])) {
$targets[$this->id] = $this->getMappingTargets();
}
$parser = feeds_importer($this->id)->parser;
if (empty($target_item)) {
$target_item = array();
}
// Many mappers add to existing fields rather than replacing them. Hence we
// need to clear target elements of each item before mapping in case we are
// mapping on a prepopulated item such as an existing node.
foreach ($this->config['mappings'] as $mapping) {
if (isset($targets[$this->id][$mapping['target']]['real_target'])) {
unset($target_item->{$targets[$this->id][$mapping['target']]['real_target']});
}
elseif (isset($target_item->{$mapping['target']})) {
unset($target_item->{$mapping['target']});
}
}
/*
This is where the actual mapping happens: For every mapping we envoke
the parser's getSourceElement() method to retrieve the value of the source
element and pass it to the processor's setTargetElement() to stick it
on the right place of the target item.
If the mapping specifies a callback method, use the callback instead of
setTargetElement().
*/
self::loadMappers();
foreach ($this->config['mappings'] as $mapping) {
// Retrieve source element's value from parser.
if (isset($sources[$this->id][$mapping['source']]) &&
is_array($sources[$this->id][$mapping['source']]) &&
isset($sources[$this->id][$mapping['source']]['callback']) &&
function_exists($sources[$this->id][$mapping['source']]['callback'])) {
$callback = $sources[$this->id][$mapping['source']]['callback'];
$value = $callback($source, $result, $mapping['source']);
}
else {
$value = $parser->getSourceElement($source, $result, $mapping['source']);
}
// Map the source element's value to the target.
if (isset($targets[$this->id][$mapping['target']]) &&
is_array($targets[$this->id][$mapping['target']]) &&
isset($targets[$this->id][$mapping['target']]['callback']) &&
function_exists($targets[$this->id][$mapping['target']]['callback'])) {
$callback = $targets[$this->id][$mapping['target']]['callback'];
$callback($source, $target_item, $mapping['target'], $value, $mapping);
}
else {
$this->setTargetElement($source, $target_item, $mapping['target'], $value, $mapping);
}
}
return $target_item;
}
/**
* Per default, don't support expiry. If processor supports expiry of imported
* items, return the time after which items should be removed.
*/
public function expiryTime() {
return FEEDS_EXPIRE_NEVER;
}
/**
* Declare default configuration.
*/
public function configDefaults() {
return array(
'mappings' => array(),
'update_existing' => FEEDS_SKIP_EXISTING,
'input_format' => NULL,
'skip_hash_check' => FALSE,
);
}
/**
* Overrides parent::configForm().
*/
public function configForm(&$form_state) {
$info = $this->entityInfo();
$form = array();
$tokens = array('@entities' => strtolower($info['label plural']));
$form['update_existing'] = array(
'#type' => 'radios',
'#title' => t('Update existing @entities', $tokens),
'#description' =>
t('Existing @entities will be determined using mappings that are a "unique target".', $tokens),
'#options' => array(
FEEDS_SKIP_EXISTING => t('Do not update existing @entities', $tokens),
FEEDS_UPDATE_EXISTING => t('Update existing @entities', $tokens),
),
'#default_value' => $this->config['update_existing'],
);
global $user;
$formats = filter_formats($user);
foreach ($formats as $format) {
$format_options[$format->format] = $format->name;
}
$form['skip_hash_check'] = array(
'#type' => 'checkbox',
'#title' => t('Skip hash check'),
'#description' => t('Force update of items even if item source data did not change.'),
'#default_value' => $this->config['skip_hash_check'],
);
$form['input_format'] = array(
'#type' => 'select',
'#title' => t('Text format'),
'#description' => t('Select the input format for the body field of the nodes to be created.'),
'#options' => $format_options,
'#default_value' => isset($this->config['input_format']) ? $this->config['input_format'] : 'plain_text',
'#required' => TRUE,
);
return $form;
}
/**
* Get mappings.
*/
public function getMappings() {
return isset($this->config['mappings']) ? $this->config['mappings'] : array();
}
/**
* Declare possible mapping targets that this processor exposes.
*
* @ingroup mappingapi
*
* @return
* An array of mapping targets. Keys are paths to targets
* separated by ->, values are TRUE if target can be unique,
* FALSE otherwise.
*/
public function getMappingTargets() {
return array(
'url' => array(
'name' => t('URL'),
'description' => t('The external URL of the item. E. g. the feed item URL in the case of a syndication feed. May be unique.'),
'optional_unique' => TRUE,
),
'guid' => array(
'name' => t('GUID'),
'description' => t('The globally unique identifier of the item. E. g. the feed item GUID in the case of a syndication feed. May be unique.'),
'optional_unique' => TRUE,
),
);
}
/**
* Set a concrete target element. Invoked from FeedsProcessor::map().
*
* @ingroup mappingapi
*/
public function setTargetElement(FeedsSource $source, $target_item, $target_element, $value) {
switch ($target_element) {
case 'url':
case 'guid':
$target_item->feeds_item->$target_element = $value;
break;
default:
$target_item->$target_element = $value;
break;
}
}
/**
* Retrieve the target entity's existing id if available. Otherwise return 0.
*
* @ingroup mappingapi
*
* @param FeedsSource $source
* The source information about this import.
* @param $result
* A FeedsParserResult object.
*
* @return
* The serial id of an entity if found, 0 otherwise.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
$query = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('feed_nid', $source->feed_nid)
->condition('entity_type', $this->entityType())
->condition('id', $source->id);
// Iterate through all unique targets and test whether they do already
// exist in the database.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
switch ($target) {
case 'url':
$entity_id = $query->condition('url', $value)->execute()->fetchField();
break;
case 'guid':
$entity_id = $query->condition('guid', $value)->execute()->fetchField();
break;
}
if (isset($entity_id)) {
// Return with the content id found.
return $entity_id;
}
}
return 0;
}
/**
* Utility function that iterates over a target array and retrieves all
* sources that are unique.
*
* @param $batch
* A FeedsImportBatch.
*
* @return
* An array where the keys are target field names and the values are the
* elements from the source item mapped to these targets.
*/
protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
$parser = feeds_importer($this->id)->parser;
$targets = array();
foreach ($this->config['mappings'] as $mapping) {
if ($mapping['unique']) {
// Invoke the parser's getSourceElement to retrieve the value for this
// mapping's source.
$targets[$mapping['target']] = $parser->getSourceElement($source, $result, $mapping['source']);
}
}
return $targets;
}
/**
* Adds Feeds specific information on $entity->feeds_item.
*
* @param $entity
* The entity object to be populated with new item info.
* @param $feed_nid
* The feed nid of the source that produces this entity.
* @param $hash
* The fingerprint of the source item.
*/
protected function newItemInfo($entity, $feed_nid, $hash = '') {
$entity->feeds_item = new stdClass();
$entity->feeds_item->entity_id = 0;
$entity->feeds_item->entity_type = $this->entityType();
$entity->feeds_item->id = $this->id;
$entity->feeds_item->feed_nid = $feed_nid;
$entity->feeds_item->imported = REQUEST_TIME;
$entity->feeds_item->hash = $hash;
$entity->feeds_item->url = '';
$entity->feeds_item->guid = '';
}
/**
* Loads existing entity information and places it on $entity->feeds_item.
*
* @param $entity
* The entity object to load item info for. Id key must be present.
*
* @return
* TRUE if item info could be loaded, false if not.
*/
protected function loadItemInfo($entity) {
$entity_info = entity_get_info($this->entityType());
$key = $entity_info['entity keys']['id'];
if ($item_info = feeds_item_info_load($this->entityType(), $entity->$key)) {
$entity->feeds_item = $item_info;
return TRUE;
}
return FALSE;
}
/**
* Create MD5 hash of item and mappings array.
*
* Include mappings as a change in mappings may have an affect on the item
* produced.
*
* @return Always returns a hash, even with empty, NULL, FALSE:
* Empty arrays return 40cd750bba9870f18aada2478b24840a
* Empty/NULL/FALSE strings return d41d8cd98f00b204e9800998ecf8427e
*/
protected function hash($item) {
static $serialized_mappings;
if (!$serialized_mappings) {
$serialized_mappings = serialize($this->config['mappings']);
}
return hash('md5', serialize($item) . $serialized_mappings);
}
/**
* Retrieves the MD5 hash of $entity_id from the database.
*
* @return string
* Empty string if no item is found, hash otherwise.
*/
protected function getHash($entity_id) {
if ($hash = db_query("SELECT hash FROM {feeds_item} WHERE entity_type = :type AND entity_id = :id", array(':type' => $this->entityType(), ':id' => $entity_id))->fetchField()) {
// Return with the hash.
return $hash;
}
return '';
}
}

View File

@@ -0,0 +1,240 @@
<?php
/**
* @file
* Contains FeedsSimplePieParser and related classes.
*/
/**
* Adapter to present SimplePie_Enclosure as FeedsEnclosure object.
*/
class FeedsSimplePieEnclosure extends FeedsEnclosure {
protected $simplepie_enclosure;
private $_serialized_simplepie_enclosure;
/**
* Constructor requires SimplePie enclosure object.
*/
function __construct(SimplePie_Enclosure $enclosure) {
$this->simplepie_enclosure = $enclosure;
}
/**
* Serialization helper.
*
* Handle the simplepie enclosure class seperately ourselves.
*/
public function __sleep() {
$this->_serialized_simplepie_enclosure = serialize($this->simplepie_enclosure);
return array('_serialized_simplepie_enclosure');
}
/**
* Unserialization helper.
*
* Ensure that the simplepie class definitions are loaded for the enclosure when unserializing.
*/
public function __wakeup() {
feeds_include_simplepie();
$this->simplepie_enclosure = unserialize($this->_serialized_simplepie_enclosure);
}
/**
* Override parent::getValue().
*/
public function getValue() {
return $this->simplepie_enclosure->get_link();
}
/**
* Override parent::getMIMEType().
*/
public function getMIMEType() {
return $this->simplepie_enclosure->get_real_type();
}
}
/**
* Class definition for Common Syndication Parser.
*
* Parses RSS and Atom feeds.
*/
class FeedsSimplePieParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
feeds_include_simplepie();
// Please be quiet SimplePie.
$level = error_reporting();
error_reporting($level ^ E_DEPRECATED ^ E_STRICT);
// Initialize SimplePie.
$parser = new SimplePie();
$parser->set_raw_data($fetcher_result->getRaw());
$parser->set_stupidly_fast(TRUE);
$parser->encode_instead_of_strip(FALSE);
// @todo Is caching effective when we pass in raw data?
$parser->enable_cache(TRUE);
$parser->set_cache_location($this->cacheDirectory());
$parser->init();
// Construct the standard form of the parsed feed
$result = new FeedsParserResult();
$result->title = html_entity_decode(($title = $parser->get_title()) ? $title : $this->createTitle($parser->get_description()));
$result->description = $parser->get_description();
$result->link = html_entity_decode($parser->get_link());
$items_num = $parser->get_item_quantity();
for ($i = 0; $i < $items_num; $i++) {
$item = array();
$simplepie_item = $parser->get_item($i);
$item['title'] = html_entity_decode(($title = $simplepie_item->get_title()) ? $title : $this->createTitle($simplepie_item->get_content()));
$item['description'] = $simplepie_item->get_content();
$item['url'] = html_entity_decode($simplepie_item->get_link());
// Use UNIX time. If no date is defined, fall back to REQUEST_TIME.
$item['timestamp'] = $simplepie_item->get_date("U");
if (empty($item['timestamp'])) {
$item['timestamp'] = REQUEST_TIME;
}
$item['guid'] = $simplepie_item->get_id();
// Use URL as GUID if there is no GUID.
if (empty($item['guid'])) {
$item['guid'] = $item['url'];
}
$author = $simplepie_item->get_author();
$item['author_name'] = isset($author->name) ? html_entity_decode($author->name) : '';
$item['author_link'] = isset($author->link) ? $author->link : '';
$item['author_email'] = isset($author->email) ? $author->email : '';
// Enclosures
$enclosures = $simplepie_item->get_enclosures();
if (is_array($enclosures)) {
foreach ($enclosures as $enclosure) {
$item['enclosures'][] = new FeedsSimplePieEnclosure($enclosure);
}
}
// Location
$latitude = $simplepie_item->get_latitude();
$longitude = $simplepie_item->get_longitude();
if (!is_null($latitude) && !is_null($longitude)) {
$item['location_latitude'][] = $latitude;
$item['location_longitude'][] = $longitude;
}
// Extract tags related to the item
$simplepie_tags = $simplepie_item->get_categories();
$tags = array();
$domains = array();
if (count($simplepie_tags) > 0) {
foreach ($simplepie_tags as $tag) {
$tags[] = (string) $tag->term;
$domain = (string) $tag->get_scheme();
if (!empty($domain)) {
if (!isset($domains[$domain])) {
$domains[$domain] = array();
}
$domains[$domain][] = count($tags) - 1;
}
}
}
$item['domains'] = $domains;
$item['tags'] = $tags;
// Allow parsing to be extended.
$this->parseExtensions($item, $simplepie_item);
$item['raw'] = $simplepie_item->data;
$result->items[] = $item;
}
// Release parser.
unset($parser);
// Set error reporting back to its previous value.
error_reporting($level);
return $result;
}
/**
* Allow extension of FeedsSimplePie item parsing.
*/
protected function parseExtensions(&$item, $simplepie_item) {}
/**
* Return mapping sources.
*/
public function getMappingSources() {
return array(
'title' => array(
'name' => t('Title'),
'description' => t('Title of the feed item.'),
),
'description' => array(
'name' => t('Description'),
'description' => t('Description of the feed item.'),
),
'author_name' => array(
'name' => t('Author name'),
'description' => t('Name of the feed item\'s author.'),
),
'author_link' => array(
'name' => t('Author link'),
'description' => t('Link to the feed item\'s author.'),
),
'author_email' => array(
'name' => t('Author email'),
'description' => t('Email address of the feed item\'s author.'),
),
'timestamp' => array(
'name' => t('Published date'),
'description' => t('Published date as UNIX time GMT of the feed item.'),
),
'url' => array(
'name' => t('Item URL (link)'),
'description' => t('URL of the feed item.'),
),
'guid' => array(
'name' => t('Item GUID'),
'description' => t('Global Unique Identifier of the feed item.'),
),
'tags' => array(
'name' => t('Categories'),
'description' => t('An array of categories that have been assigned to the feed item.'),
),
'domains' => array(
'name' => t('Category domains'),
'description' => t('Domains of the categories.'),
),
'location_latitude' => array(
'name' => t('Latitudes'),
'description' => t('An array of latitudes assigned to the feed item.'),
),
'location_longitude' => array(
'name' => t('Longitudes'),
'description' => t('An array of longitudes assigned to the feed item.'),
),
'enclosures' => array(
'name' => t('Enclosures'),
'description' => t('An array of enclosures attached to the feed item.'),
),
) + parent::getMappingSources();
}
/**
* Returns cache directory. Creates it if it doesn't exist.
*/
protected function cacheDirectory() {
$directory = 'public://simplepie';
file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
return $directory;
}
/**
* Generate a title from a random text.
*/
protected function createTitle($text = FALSE) {
// Explode to words and use the first 3 words.
$words = preg_split("/[\s,]+/", $text);
$words = array_slice($words, 0, 3);
return implode(' ', $words);
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains FeedsSitemapParser and related classes.
*/
/**
* A parser for the Sitemap specification http://www.sitemaps.org/protocol.php
*/
class FeedsSitemapParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
// Set time zone to GMT for parsing dates with strtotime().
$tz = date_default_timezone_get();
date_default_timezone_set('GMT');
// Yes, using a DOM parser is a bit inefficient, but will do for now
$xml = new SimpleXMLElement($fetcher_result->getRaw());
$result = new FeedsParserResult();
foreach ($xml->url as $url) {
$item = array('url' => (string) $url->loc);
if ($url->lastmod) {
$item['lastmod'] = strtotime($url->lastmod);
}
if ($url->changefreq) {
$item['changefreq'] = (string) $url->changefreq;
}
if ($url->priority) {
$item['priority'] = (string) $url->priority;
}
$result->items[] = $item;
}
date_default_timezone_set($tz);
return $result;
}
/**
* Implements FeedsParser::getMappingSources().
*/
public function getMappingSources() {
return array(
'url' => array(
'name' => t('Item URL (link)'),
'description' => t('URL of the feed item.'),
),
'lastmod' => array(
'name' => t('Last modification date'),
'description' => t('Last modified date as UNIX time GMT of the feed item.'),
),
'changefreq' => array(
'name' => t('Change frequency'),
'description' => t('How frequently the page is likely to change.'),
),
'priority' => array(
'name' => t('Priority'),
'description' => t('The priority of this URL relative to other URLs on the site.'),
),
) + parent::getMappingSources();
}
}

View File

@@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains FeedsSyndicationParser and related classes.
*/
/**
* Class definition for Common Syndication Parser.
*
* Parses RSS and Atom feeds.
*/
class FeedsSyndicationParser extends FeedsParser {
/**
* Implements FeedsParser::parse().
*/
public function parse(FeedsSource $source, FeedsFetcherResult $fetcher_result) {
feeds_include_library('common_syndication_parser.inc', 'common_syndication_parser');
$feed = common_syndication_parser_parse($fetcher_result->getRaw());
$result = new FeedsParserResult();
$result->title = $feed['title'];
$result->description = $feed['description'];
$result->link = $feed['link'];
if (is_array($feed['items'])) {
foreach ($feed['items'] as $item) {
if (isset($item['geolocations'])) {
foreach ($item['geolocations'] as $k => $v) {
$item['geolocations'][$k] = new FeedsGeoTermElement($v);
}
}
$result->items[] = $item;
}
}
return $result;
}
/**
* Return mapping sources.
*
* At a future point, we could expose data type information here,
* storage systems like Data module could use this information to store
* parsed data automatically in fields with a correct field type.
*/
public function getMappingSources() {
return array(
'title' => array(
'name' => t('Title'),
'description' => t('Title of the feed item.'),
),
'description' => array(
'name' => t('Description'),
'description' => t('Description of the feed item.'),
),
'author_name' => array(
'name' => t('Author name'),
'description' => t('Name of the feed item\'s author.'),
),
'timestamp' => array(
'name' => t('Published date'),
'description' => t('Published date as UNIX time GMT of the feed item.'),
),
'url' => array(
'name' => t('Item URL (link)'),
'description' => t('URL of the feed item.'),
),
'guid' => array(
'name' => t('Item GUID'),
'description' => t('Global Unique Identifier of the feed item.'),
),
'tags' => array(
'name' => t('Categories'),
'description' => t('An array of categories that have been assigned to the feed item.'),
),
'geolocations' => array(
'name' => t('Geo Locations'),
'description' => t('An array of geographic locations with a name and a position.'),
),
) + parent::getMappingSources();
}
}

View File

@@ -0,0 +1,245 @@
<?php
/**
* @file
* FeedsTermProcessor class.
*/
/**
* Feeds processor plugin. Create taxonomy terms from feed items.
*/
class FeedsTermProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
public function entityType() {
return 'taxonomy_term';
}
/**
* Implements parent::entityInfo().
*/
protected function entityInfo() {
$info = parent::entityInfo();
$info['label plural'] = t('Terms');
return $info;
}
/**
* Creates a new term in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$vocabulary = $this->vocabulary();
$term = new stdClass();
$term->vid = $vocabulary->vid;
$term->vocabulary_machine_name = $vocabulary->machine_name;
$term->format = isset($this->config['input_format']) ? $this->config['input_format'] : filter_fallback_format();
return $term;
}
/**
* Loads an existing term.
*/
protected function entityLoad(FeedsSource $source, $tid) {
return taxonomy_term_load($tid);
}
/**
* Validates a term.
*/
protected function entityValidate($term) {
if (empty($term->name)) {
throw new FeedsValidationException(t('Term name missing.'));
}
}
/**
* Saves a term.
*
* We de-array parent fields with only one item.
* This stops leftandright module from freaking out.
*/
protected function entitySave($term) {
if (isset($term->parent)) {
if (is_array($term->parent) && count($term->parent) == 1) {
$term->parent = reset($term->parent);
}
if (isset($term->tid) && ($term->parent == $term->tid || (is_array($term->parent) && in_array($term->tid, $term->parent)))) {
throw new FeedsValidationException(t("A term can't be its own child. GUID:@guid", array('@guid' => $term->feeds_item->guid)));
}
}
taxonomy_term_save($term);
}
/**
* Deletes a series of terms.
*/
protected function entityDeleteMultiple($tids) {
foreach ($tids as $tid) {
taxonomy_term_delete($tid);
}
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'vocabulary' => 0,
) + parent::configDefaults();
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$options = array(0 => t('Select a vocabulary'));
foreach (taxonomy_get_vocabularies() as $vocab) {
$options[$vocab->machine_name] = $vocab->name;
}
$form = parent::configForm($form_state);
$form['vocabulary'] = array(
'#type' => 'select',
'#title' => t('Import to vocabulary'),
'#description' => t('Choose the vocabulary to import into. <strong>CAUTION:</strong> when deleting terms through the "Delete items" tab, Feeds will delete <em>all</em> terms from this vocabulary.'),
'#options' => $options,
'#default_value' => $this->config['vocabulary'],
);
return $form;
}
/**
* Override parent::configFormValidate().
*/
public function configFormValidate(&$values) {
if (empty($values['vocabulary'])) {
form_set_error('vocabulary', t('Choose a vocabulary'));
}
}
/**
* Override setTargetElement to operate on a target item that is a taxonomy term.
*/
public function setTargetElement(FeedsSource $source, $target_term, $target_element, $value) {
switch ($target_element) {
case 'parent':
if (!empty($value)) {
$terms = taxonomy_get_term_by_name($value);
$parent_tid = '';
foreach ($terms as $term) {
if ($term->vid == $target_term->vid) {
$parent_tid = $term->tid;
}
}
if (!empty($parent_tid)) {
$target_term->parent[] = $parent_tid;
}
else {
$target_term->parent[] = 0;
}
}
else {
$target_term->parent[] = 0;
}
break;
case 'parentguid':
// value is parent_guid field value
$query = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('entity_type', $this->entityType());
$parent_tid = $query->condition('guid', $value)->execute()->fetchField();
$target_term->parent[] = ($parent_tid) ? $parent_tid : 0;
break;
case 'weight':
if (!empty($value)) {
$weight = intval($value);
}
else {
$weight = 0;
}
$target_term->weight = $weight;
break;
default:
parent::setTargetElement($source, $target_term, $target_element, $value);
break;
}
}
/**
* Return available mapping targets.
*/
public function getMappingTargets() {
$targets = parent::getMappingTargets();
$targets += array(
'name' => array(
'name' => t('Term name'),
'description' => t('Name of the taxonomy term.'),
'optional_unique' => TRUE,
),
'parent' => array(
'name' => t('Parent: Term name'),
'description' => t('The name of the parent taxonomy term.'),
'optional_unique' => TRUE,
),
'parentguid' => array(
'name' => t('Parent: GUID'),
'description' => t('The GUID of the parent taxonomy term.'),
'optional_unique' => TRUE,
),
'weight' => array(
'name' => t('Term weight'),
'description' => t('Weight of the taxonomy term.'),
'optional_unique' => TRUE,
),
'description' => array(
'name' => t('Term description'),
'description' => t('Description of the taxonomy term.'),
),
);
// Let implementers of hook_feeds_term_processor_targets() add their targets.
try {
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->vocabulary()->machine_name;
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
}
catch (Exception $e) {
// Do nothing.
}
return $targets;
}
/**
* Get id of an existing feed item term if available.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
if ($tid = parent::existingEntityId($source, $result)) {
return $tid;
}
// The only possible unique target is name.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
if ($target == 'name') {
$vocabulary = $this->vocabulary();
if ($tid = db_query("SELECT tid FROM {taxonomy_term_data} WHERE name = :name AND vid = :vid", array(':name' => $value, ':vid' => $vocabulary->vid))->fetchField()) {
return $tid;
}
}
}
return 0;
}
/**
* Return vocabulary to map to.
*/
public function vocabulary() {
if (isset($this->config['vocabulary'])) {
if ($vocabulary = taxonomy_vocabulary_machine_name_load($this->config['vocabulary'])) {
return $vocabulary;
}
}
throw new Exception(t('No vocabulary defined for Taxonomy Term processor.'));
}
}

View File

@@ -0,0 +1,242 @@
<?php
/**
* @file
* FeedsUserProcessor class.
*/
/**
* Feeds processor plugin. Create users from feed items.
*/
class FeedsUserProcessor extends FeedsProcessor {
/**
* Define entity type.
*/
public function entityType() {
return 'user';
}
/**
* Implements parent::entityInfo().
*/
protected function entityInfo() {
$info = parent::entityInfo();
$info['label plural'] = t('Users');
return $info;
}
/**
* Creates a new user account in memory and returns it.
*/
protected function newEntity(FeedsSource $source) {
$account = new stdClass();
$account->uid = 0;
$account->roles = array_filter($this->config['roles']);
$account->status = $this->config['status'];
return $account;
}
/**
* Loads an existing user.
*/
protected function entityLoad(FeedsSource $source, $uid) {
// Copy the password so that we can compare it again at save.
$user = user_load($uid);
$user->feeds_original_pass = $user->pass;
return $user;
}
/**
* Validates a user account.
*/
protected function entityValidate($account) {
if (empty($account->name) || empty($account->mail) || !valid_email_address($account->mail)) {
throw new FeedsValidationException(t('User name missing or email not valid.'));
}
}
/**
* Save a user account.
*/
protected function entitySave($account) {
if ($this->config['defuse_mail']) {
$account->mail = $account->mail . '_test';
}
$edit = (array) $account;
// Remove pass from $edit if the password is unchanged.
if (isset($account->feeds_original_pass) && $account->pass == $account->feeds_original_pass) {
unset($edit['pass']);
}
user_save($account, $edit);
if ($account->uid && !empty($account->openid)) {
$authmap = array(
'uid' => $account->uid,
'module' => 'openid',
'authname' => $account->openid,
);
if (SAVED_UPDATED != drupal_write_record('authmap', $authmap, array('uid', 'module'))) {
drupal_write_record('authmap', $authmap);
}
}
}
/**
* Delete multiple user accounts.
*/
protected function entityDeleteMultiple($uids) {
foreach ($uids as $uid) {
user_delete($uid);
}
}
/**
* Override parent::configDefaults().
*/
public function configDefaults() {
return array(
'roles' => array(),
'status' => 1,
'defuse_mail' => FALSE,
) + parent::configDefaults();
}
/**
* Override parent::configForm().
*/
public function configForm(&$form_state) {
$form = parent::configForm($form_state);
$form['status'] = array(
'#type' => 'radios',
'#title' => t('Status'),
'#description' => t('Select whether users should be imported active or blocked.'),
'#options' => array(0 => t('Blocked'), 1 => t('Active')),
'#default_value' => $this->config['status'],
);
$roles = user_roles(TRUE);
unset($roles[2]);
if (count($roles)) {
$form['roles'] = array(
'#type' => 'checkboxes',
'#title' => t('Additional roles'),
'#description' => t('Every user is assigned the "authenticated user" role. Select additional roles here.'),
'#default_value' => $this->config['roles'],
'#options' => $roles,
);
}
// @todo Implement true updating.
$form['update_existing'] = array(
'#type' => 'checkbox',
'#title' => t('Replace existing users'),
'#description' => t('If an existing user is found for an imported user, replace it. Existing users will be determined using mappings that are a "unique target".'),
'#default_value' => $this->config['update_existing'],
);
$form['defuse_mail'] = array(
'#type' => 'checkbox',
'#title' => t('Defuse e-mail addresses'),
'#description' => t('This appends _test to all imported e-mail addresses to ensure they cannot be used as recipients.'),
'#default_value' => $this->config['defuse_mail'],
);
return $form;
}
/**
* Override setTargetElement to operate on a target item that is a node.
*/
public function setTargetElement(FeedsSource $source, $target_user, $target_element, $value) {
switch ($target_element) {
case 'created':
$target_user->created = feeds_to_unixtime($value, REQUEST_TIME);
break;
case 'language':
$target_user->language = strtolower($value);
break;
default:
parent::setTargetElement($source, $target_user, $target_element, $value);
break;
}
}
/**
* Return available mapping targets.
*/
public function getMappingTargets() {
$targets = parent::getMappingTargets();
$targets += array(
'name' => array(
'name' => t('User name'),
'description' => t('Name of the user.'),
'optional_unique' => TRUE,
),
'mail' => array(
'name' => t('Email address'),
'description' => t('Email address of the user.'),
'optional_unique' => TRUE,
),
'created' => array(
'name' => t('Created date'),
'description' => t('The created (e. g. joined) data of the user.'),
),
'pass' => array(
'name' => t('Unencrypted Password'),
'description' => t('The unencrypted user password.'),
),
'status' => array(
'name' => t('Account status'),
'description' => t('Whether a user is active or not. 1 stands for active, 0 for blocked.'),
),
'language' => array(
'name' => t('User language'),
'description' => t('Default language for the user.'),
),
);
if (module_exists('openid')) {
$targets['openid'] = array(
'name' => t('OpenID identifier'),
'description' => t('The OpenID identifier of the user. <strong>CAUTION:</strong> Use only for migration purposes, misconfiguration of the OpenID identifier can lead to severe security breaches like users gaining access to accounts other than their own.'),
'optional_unique' => TRUE,
);
}
// Let other modules expose mapping targets.
self::loadMappers();
$entity_type = $this->entityType();
$bundle = $this->entityType();
drupal_alter('feeds_processor_targets', $targets, $entity_type, $bundle);
return $targets;
}
/**
* Get id of an existing feed item term if available.
*/
protected function existingEntityId(FeedsSource $source, FeedsParserResult $result) {
if ($uid = parent::existingEntityId($source, $result)) {
return $uid;
}
// Iterate through all unique targets and try to find a user for the
// target's value.
foreach ($this->uniqueTargets($source, $result) as $target => $value) {
switch ($target) {
case 'name':
$uid = db_query("SELECT uid FROM {users} WHERE name = :name", array(':name' => $value))->fetchField();
break;
case 'mail':
$uid = db_query("SELECT uid FROM {users} WHERE mail = :mail", array(':mail' => $value))->fetchField();
break;
case 'openid':
$uid = db_query("SELECT uid FROM {authmap} WHERE authname = :authname AND module = 'openid'", array(':authname' => $value))->fetchField();
break;
}
if ($uid) {
// Return with the first nid found.
return $uid;
}
}
return 0;
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* @file
* Tests for the common syndication parser.
*/
/**
* Test cases for common syndication parser library.
*
* @todo Break out into Drupal independent test framework.
* @todo Could I use DrupalUnitTestCase here?
*/
class CommonSyndicationParserTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Common Syndication Parser',
'description' => 'Unit tests for Common Syndication Parser.',
'group' => 'Feeds',
);
}
public function setUp() {
parent::setUp(array('feeds', 'feeds_ui', 'ctools', 'job_scheduler'));
feeds_include_library('common_syndication_parser.inc', 'common_syndication_parser');
}
/**
* Dispatch tests, only use one entry point method testX to save time.
*/
public function test() {
$this->_testRSS10();
$this->_testRSS2();
$this->_testAtomGeoRSS();
}
/**
* Test RSS 1.0.
*/
protected function _testRSS10() {
$string = $this->readFeed('magento.rss1');
$feed = common_syndication_parser_parse($string);
$this->assertEqual($feed['title'], 'Magento Sites Network - A directory listing of Magento Commerce stores');
$this->assertEqual($feed['items'][0]['title'], 'Gezondheidswebwinkel');
$this->assertEqual($feed['items'][0]['url'], 'http://www.magentosites.net/store/2010/04/28/gezondheidswebwinkel/index.html');
$this->assertEqual($feed['items'][1]['url'], 'http://www.magentosites.net/store/2010/04/26/mybobinocom/index.html');
$this->assertEqual($feed['items'][1]['guid'], 'http://www.magentosites.net/node/3472');
$this->assertEqual($feed['items'][2]['guid'], 'http://www.magentosites.net/node/3471');
$this->assertEqual($feed['items'][2]['timestamp'], 1272285294);
}
/**
* Test RSS 2.
*/
protected function _testRSS2() {
$string = $this->readFeed('developmentseed.rss2');
$feed = common_syndication_parser_parse($string);
$this->assertEqual($feed['title'], 'Development Seed - Technological Solutions for Progressive Organizations');
$this->assertEqual($feed['items'][0]['title'], 'Open Atrium Translation Workflow: Two Way Translation Updates');
$this->assertEqual($feed['items'][1]['url'], 'http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition');
$this->assertEqual($feed['items'][1]['guid'], '973 at http://developmentseed.org');
$this->assertEqual($feed['items'][2]['guid'], '972 at http://developmentseed.org');
$this->assertEqual($feed['items'][2]['timestamp'], 1254493864);
}
/**
* Test Geo RSS in Atom feed.
*/
protected function _testAtomGeoRSS() {
$string = $this->readFeed('earthquake-georss.atom');
$feed = common_syndication_parser_parse($string);
$this->assertEqual($feed['title'], 'USGS M2.5+ Earthquakes');
$this->assertEqual($feed['items'][0]['title'], 'M 2.6, Central Alaska');
$this->assertEqual($feed['items'][1]['url'], 'http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axbz.php');
$this->assertEqual($feed['items'][1]['guid'], 'urn:earthquake-usgs-gov:us:2010axbz');
$this->assertEqual($feed['items'][2]['guid'], 'urn:earthquake-usgs-gov:us:2010axbr');
$this->assertEqual($feed['items'][2]['geolocations'][0]['name'], '-53.1979 -118.0676');
$this->assertEqual($feed['items'][2]['geolocations'][0]['lat'], '-53.1979');
$this->assertEqual($feed['items'][2]['geolocations'][0]['lon'], '-118.0676');
$this->assertEqual($feed['items'][3]['geolocations'][0]['name'], '-43.4371 172.5902');
$this->assertEqual($feed['items'][3]['geolocations'][0]['lat'], '-43.4371');
$this->assertEqual($feed['items'][3]['geolocations'][0]['lon'], '172.5902');
}
/**
* Helper to read a feed.
*/
protected function readFeed($filename) {
$feed = dirname(__FILE__) . '/feeds/' . $filename;
$handle = fopen($feed, 'r');
$string = fread($handle, filesize($feed));
fclose($handle);
return $string;
}
}

View File

@@ -0,0 +1,674 @@
<?php
/**
* @file
* Common functionality for all Feeds tests.
*/
/**
* Test basic Data API functionality.
*/
class FeedsWebTestCase extends DrupalWebTestCase {
protected $profile = 'testing';
public function setUp() {
$args = func_get_args();
// Build the list of required modules which can be altered by passing in an
// array of module names to setUp().
if (isset($args[0])) {
if (is_array($args[0])) {
$modules = $args[0];
}
else {
$modules = $args;
}
}
else {
$modules = array();
}
$modules[] = 'taxonomy';
$modules[] = 'image';
$modules[] = 'file';
$modules[] = 'field';
$modules[] = 'field_ui';
$modules[] = 'feeds';
$modules[] = 'feeds_ui';
$modules[] = 'feeds_tests';
$modules[] = 'ctools';
$modules[] = 'job_scheduler';
$modules = array_unique($modules);
parent::setUp($modules);
// Add text formats Directly.
$filtered_html_format = array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(
// URL filter.
'filter_url' => array(
'weight' => 0,
'status' => 1,
),
// HTML filter.
'filter_html' => array(
'weight' => 1,
'status' => 1,
),
// Line break filter.
'filter_autop' => array(
'weight' => 2,
'status' => 1,
),
// HTML corrector filter.
'filter_htmlcorrector' => array(
'weight' => 10,
'status' => 1,
),
),
);
$filtered_html_format = (object) $filtered_html_format;
filter_format_save($filtered_html_format);
// Build the list of required administration permissions. Additional
// permissions can be passed as an array into setUp()'s second parameter.
if (isset($args[1]) && is_array($args[1])) {
$permissions = $args[1];
}
else {
$permissions = array();
}
$permissions[] = 'access content';
$permissions[] = 'administer site configuration';
$permissions[] = 'administer content types';
$permissions[] = 'administer nodes';
$permissions[] = 'bypass node access';
$permissions[] = 'administer taxonomy';
$permissions[] = 'administer users';
$permissions[] = 'administer feeds';
// Create an admin user and log in.
$this->admin_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->admin_user);
$types = array(
array(
'type' => 'page',
'name' => 'Basic page',
'node_options[status]' => 1,
'node_options[promote]' => 0,
),
array(
'type' => 'article',
'name' => 'Article',
'node_options[status]' => 1,
'node_options[promote]' => 1,
),
);
foreach ($types as $type) {
$this->drupalPost('admin/structure/types/add', $type, 'Save content type');
$this->assertText("The content type " . $type['name'] . " has been added.");
}
}
/**
* Absolute path to Drupal root.
*/
public function absolute() {
return realpath(getcwd());
}
/**
* Get the absolute directory path of the feeds module.
*/
public function absolutePath() {
return $this->absolute() . '/' . drupal_get_path('module', 'feeds');
}
/**
* Generate an OPML test feed.
*
* The purpose of this function is to create a dynamic OPML feed that points
* to feeds included in this test.
*/
public function generateOPML() {
$path = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'feeds') . '/tests/feeds/';
$output =
'<?xml version="1.0" encoding="utf-8"?>
<opml version="1.1">
<head>
<title>Feeds test OPML</title>
<dateCreated>Fri, 16 Oct 2009 02:53:17 GMT</dateCreated>
<ownerName></ownerName>
</head>
<body>
<outline text="Feeds test group" >
<outline title="Development Seed - Technological Solutions for Progressive Organizations" text="" xmlUrl="' . $path . 'developmentseed.rss2" type="rss" />
<outline title="Magyar Nemzet Online - H\'rek" text="" xmlUrl="' . $path . 'feed_without_guid.rss2" type="rss" />
<outline title="Drupal planet" text="" type="rss" xmlUrl="' . $path . 'drupalplanet.rss2" />
</outline>
</body>
</opml>';
// UTF 8 encode output string and write it to disk
$output = utf8_encode($output);
$filename = file_default_scheme() . '://test-opml-' . $this->randomName() . '.opml';
$filename = file_unmanaged_save_data($output, $filename);
return $filename;
}
/**
* Create an importer configuration.
*
* @param $name
* The natural name of the feed.
* @param $id
* The persistent id of the feed.
* @param $edit
* Optional array that defines the basic settings for the feed in a format
* that can be posted to the feed's basic settings form.
*/
public function createImporterConfiguration($name = 'Syndication', $id = 'syndication') {
// Create new feed configuration.
$this->drupalGet('admin/structure/feeds');
$this->clickLink('Add importer');
$edit = array(
'name' => $name,
'id' => $id,
);
$this->drupalPost('admin/structure/feeds/create', $edit, 'Create');
// Assert message and presence of default plugins.
$this->assertText('Your configuration has been created with default settings.');
$this->assertPlugins($id, 'FeedsHTTPFetcher', 'FeedsSyndicationParser', 'FeedsNodeProcessor');
// Per default attach to page content type.
$this->setSettings($id, NULL, array('content_type' => 'page'));
}
/**
* Choose a plugin for a importer configuration and assert it.
*
* @param $id
* The importer configuration's id.
* @param $plugin_key
* The key string of the plugin to choose (one of the keys defined in
* feeds_feeds_plugins()).
*/
public function setPlugin($id, $plugin_key) {
if ($type = FeedsPlugin::typeOf($plugin_key)) {
$edit = array(
'plugin_key' => $plugin_key,
);
$this->drupalPost("admin/structure/feeds/$id/$type", $edit, 'Save');
// Assert actual configuration.
$config = unserialize(db_query("SELECT config FROM {feeds_importer} WHERE id = :id", array(':id' => $id))->fetchField());
$this->assertEqual($config[$type]['plugin_key'], $plugin_key, 'Verified correct ' . $type . ' (' . $plugin_key . ').');
}
}
/**
* Set importer or plugin settings.
*
* @param $id
* The importer configuration's id.
* @param $plugin
* The plugin (class) name, or NULL to set importer's settings
* @param $settings
* The settings to set.
*/
public function setSettings($id, $plugin, $settings) {
$this->drupalPost('admin/structure/feeds/' . $id . '/settings/' . $plugin, $settings, 'Save');
$this->assertText('Your changes have been saved.');
}
/**
* Create a test feed node. Test user has to have sufficient permissions:
*
* * create [type] content
* * use feeds
*
* Assumes that page content type has been configured with
* createImporterConfiguration() as a feed content type.
*
* @return
* The node id of the node created.
*/
public function createFeedNode($id = 'syndication', $feed_url = NULL, $title = '', $content_type = NULL) {
if (empty($feed_url)) {
$feed_url = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'feeds') . '/tests/feeds/developmentseed.rss2';
}
// If content type not given, retrieve it.
if (!$content_type) {
$result= db_select('feeds_importer', 'f')
->condition('f.id', $id, '=')
->fields('f', array('config'))
->execute();
$config = unserialize($result->fetchField());
$content_type = $config['content_type'];
$this->assertFalse(empty($content_type), 'Valid content type found: ' . $content_type);
}
// Create a feed node.
$edit = array(
'title' => $title,
'feeds[FeedsHTTPFetcher][source]' => $feed_url,
);
$this->drupalPost('node/add/' . str_replace('_', '-', $content_type), $edit, 'Save');
$this->assertText('has been created.');
// Get the node id from URL.
$nid = $this->getNid($this->getUrl());
// Check whether feed got recorded in feeds_source table.
$query = db_select('feeds_source', 's')
->condition('s.id', $id, '=')
->condition('s.feed_nid', $nid, '=');
$query->addExpression("COUNT(*)");
$result = $query->execute()->fetchField();
$this->assertEqual(1, $result);
$source = db_select('feeds_source', 's')
->condition('s.id', $id, '=')
->condition('s.feed_nid', $nid, '=')
->fields('s', array('config'))
->execute()->fetchObject();
$config = unserialize($source->config);
$this->assertEqual($config['FeedsHTTPFetcher']['source'], $feed_url, t('URL in DB correct.'));
return $nid;
}
/**
* Edit the configuration of a feed node to test update behavior.
*
* @param $nid
* The nid to edit.
* @param $feed_url
* The new (absolute) feed URL to use.
* @param $title
* Optional parameter to change title of feed node.
*/
public function editFeedNode($nid, $feed_url, $title = '') {
$edit = array(
'title' => $title,
'feeds[FeedsHTTPFetcher][source]' => $feed_url,
);
// Check that the update was saved.
$this->drupalPost('node/' . $nid . '/edit', $edit, 'Save');
$this->assertText('has been updated.');
// Check that the URL was updated in the feeds_source table.
$source = db_query("SELECT * FROM {feeds_source} WHERE feed_nid = :nid", array(':nid' => $nid))->fetchObject();
$config = unserialize($source->config);
$this->assertEqual($config['FeedsHTTPFetcher']['source'], $feed_url, t('URL in DB correct.'));
}
/**
* Batch create a variable amount of feed nodes. All will have the
* same URL configured.
*
* @return
* An array of node ids of the nodes created.
*/
public function createFeedNodes($id = 'syndication', $num = 20, $content_type = NULL) {
$nids = array();
for ($i = 0; $i < $num; $i++) {
$nids[] = $this->createFeedNode($id, NULL, $this->randomName(), $content_type);
}
return $nids;
}
/**
* Import a URL through the import form. Assumes FeedsHTTPFetcher in place.
*/
public function importURL($id, $feed_url = NULL) {
if (empty($feed_url)) {
$feed_url = $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'feeds') . '/tests/feeds/developmentseed.rss2';
}
$edit = array(
'feeds[FeedsHTTPFetcher][source]' => $feed_url,
);
$nid = $this->drupalPost('import/' . $id, $edit, 'Import');
// Check whether feed got recorded in feeds_source table.
$this->assertEqual(1, db_query("SELECT COUNT(*) FROM {feeds_source} WHERE id = :id AND feed_nid = 0", array(':id' => $id))->fetchField());
$source = db_query("SELECT * FROM {feeds_source} WHERE id = :id AND feed_nid = 0", array(':id' => $id))->fetchObject();
$config = unserialize($source->config);
$this->assertEqual($config['FeedsHTTPFetcher']['source'], $feed_url, t('URL in DB correct.'));
// Check whether feed got properly added to scheduler.
$this->assertEqual(1, db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = :id AND id = 0 AND name = 'feeds_source_import' AND last <> 0 AND scheduled = 0", array(':id' => $id))->fetchField());
// There must be only one entry for callback 'expire' - no matter what the feed_nid is.
$this->assertEqual(0, db_query("SELECT COUNT(*) FROM {job_schedule} WHERE type = :id AND name = 'feeds_importer_expire' AND last <> 0 AND scheduled = 0", array(':id' => $id))->fetchField());
}
/**
* Import a file through the import form. Assumes FeedsFileFetcher in place.
*/
public function importFile($id, $file) {
$this->assertTrue(file_exists($file), 'Source file exists');
$edit = array(
'files[feeds]' => $file,
);
$this->drupalPost('import/' . $id, $edit, 'Import');
}
/**
* Assert a feeds configuration's plugins.
*
* @deprecated:
* Use setPlugin() instead.
*
* @todo Refactor users of assertPlugin() and make them use setPugin() instead.
*/
public function assertPlugins($id, $fetcher, $parser, $processor) {
// Assert actual configuration.
$config = unserialize(db_query("SELECT config FROM {feeds_importer} WHERE id = :id", array(':id' => $id))->fetchField());
$this->assertEqual($config['fetcher']['plugin_key'], $fetcher, 'Correct fetcher');
$this->assertEqual($config['parser']['plugin_key'], $parser, 'Correct parser');
$this->assertEqual($config['processor']['plugin_key'], $processor, 'Correct processor');
}
/**
* Adds mappings to a given configuration.
*
* @param string $id
* ID of the importer.
* @param array $mappings
* An array of mapping arrays. Each mapping array must have a source and
* an target key and can have a unique key.
* @param bool $test_mappings
* (optional) TRUE to automatically test mapping configs. Defaults to TRUE.
*/
public function addMappings($id, $mappings, $test_mappings = TRUE) {
$path = "admin/structure/feeds/$id/mapping";
// Iterate through all mappings and add the mapping via the form.
foreach ($mappings as $i => $mapping) {
if ($test_mappings) {
$current_mapping_key = $this->mappingExists($id, $i, $mapping['source'], $mapping['target']);
$this->assertEqual($current_mapping_key, -1, 'Mapping does not exist before addition.');
}
// Get unique flag and unset it. Otherwise, drupalPost will complain that
// Split up config and mapping.
$config = $mapping;
unset($config['source'], $config['target']);
$mapping = array('source' => $mapping['source'], 'target' => $mapping['target']);
// Add mapping.
$this->drupalPost($path, $mapping, t('Add'));
// If there are other configuration options, set them.
if ($config) {
$this->drupalPostAJAX(NULL, array(), 'mapping_settings_edit_' . $i);
// Set some settings.
$edit = array();
foreach ($config as $key => $value) {
$edit["config[$i][settings][$key]"] = $value;
}
$this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_' . $i);
$this->drupalPost(NULL, array(), t('Save'));
}
if ($test_mappings) {
$current_mapping_key = $this->mappingExists($id, $i, $mapping['source'], $mapping['target']);
$this->assertTrue($current_mapping_key >= 0, 'Mapping exists after addition.');
}
}
}
/**
* Remove mappings from a given configuration.
*
* This function mimicks the Javascript behavior in feeds_ui.js
*
* @param array $mappings
* An array of mapping arrays. Each mapping array must have a source and
* a target key and can have a unique key.
* @param bool $test_mappings
* (optional) TRUE to automatically test mapping configs. Defaults to TRUE.
*/
public function removeMappings($id, $mappings, $test_mappings = TRUE) {
$path = "admin/structure/feeds/$id/mapping";
$current_mappings = $this->getCurrentMappings($id);
// Iterate through all mappings and remove via the form.
foreach ($mappings as $i => $mapping) {
if ($test_mappings) {
$current_mapping_key = $this->mappingExists($id, $i, $mapping['source'], $mapping['target']);
$this->assertEqual($current_mapping_key, $i, 'Mapping exists before removal.');
}
$remove_mapping = array("remove_flags[$i]" => 1);
$this->drupalPost($path, $remove_mapping, t('Save'));
$this->assertText('Your changes have been saved.');
if ($test_mappings) {
$current_mapping_key = $this->mappingExists($id, $i, $mapping['source'], $mapping['target']);
$this->assertEqual($current_mapping_key, -1, 'Mapping does not exist after removal.');
}
}
}
/**
* Gets an array of current mappings from the feeds_importer config.
*
* @param string $id
* ID of the importer.
*
* @return bool|array
* FALSE if the importer has no mappings, or an an array of mappings.
*/
public function getCurrentMappings($id) {
$config = db_query("SELECT config FROM {feeds_importer} WHERE id = :id", array(':id' => $id))->fetchField();
$config = unserialize($config);
// We are very specific here. 'mappings' can either be an array or not
// exist.
if (array_key_exists('mappings', $config['processor']['config'])) {
$this->assertTrue(is_array($config['processor']['config']['mappings']), 'Mappings is an array.');
return $config['processor']['config']['mappings'];
}
return FALSE;
}
/**
* Determines if a mapping exists for a given importer.
*
* @param string $id
* ID of the importer.
* @param integer $i
* The key of the mapping.
* @param string $source
* The source field.
* @param string $target
* The target field.
*
* @return integer
* -1 if the mapping doesn't exist, the key of the mapping otherwise.
*/
public function mappingExists($id, $i, $source, $target) {
$current_mappings = $this->getCurrentMappings($id);
if ($current_mappings) {
foreach ($current_mappings as $key => $mapping) {
if ($mapping['source'] == $source && $mapping['target'] == $target && $key == $i) {
return $key;
}
}
}
return -1;
}
/**
* Helper function, retrieves node id from a URL.
*/
public function getNid($url) {
$matches = array();
preg_match('/node\/(\d+?)$/', $url, $matches);
$nid = $matches[1];
// Test for actual integerness.
$this->assertTrue($nid === (string) (int) $nid, 'Node id is an integer.');
return $nid;
}
/**
* Copies a directory.
*/
public function copyDir($source, $dest) {
$result = file_prepare_directory($dest, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
foreach (@scandir($source) as $file) {
if (is_file("$source/$file")) {
$file = file_unmanaged_copy("$source/$file", "$dest/$file");
}
}
}
/**
* Download and extract SimplePIE.
*
* Sets the 'feeds_simplepie_library_dir' variable to the directory where
* SimplePie is downloaded.
*/
function downloadExtractSimplePie($version) {
$url = "http://simplepie.org/downloads/simplepie_$version.mini.php";
$filename = 'simplepie.mini.php';
// Avoid downloading the file dozens of times
$library_dir = DRUPAL_ROOT . '/' . $this->originalFileDirectory . '/simpletest/feeds';
$simplepie_library_dir = $library_dir . '/simplepie';
if (!file_exists($library_dir)) {
drupal_mkdir($library_dir);
}
if (!file_exists($simplepie_library_dir)) {
drupal_mkdir($simplepie_library_dir);
}
// Local file name.
$local_file = $simplepie_library_dir . '/' . $filename;
// Begin single threaded code.
if (function_exists('sem_get')) {
$semaphore = sem_get(ftok(__FILE__, 1));
sem_acquire($semaphore);
}
// Download and extact the archive, but only in one thread.
if (!file_exists($local_file)) {
$local_file = system_retrieve_file($url, $local_file, FALSE, FILE_EXISTS_REPLACE);
}
if (function_exists('sem_get')) {
sem_release($semaphore);
}
// End single threaded code.
// Verify that files were successfully extracted.
$this->assertTrue(file_exists($local_file), t('@file found.', array('@file' => $local_file)));
// Set the simpletest library directory.
variable_set('feeds_library_dir', $library_dir);
}
}
/**
* Provides a wrapper for DrupalUnitTestCase for Feeds unit testing.
*/
class FeedsUnitTestHelper extends DrupalUnitTestCase {
public function setUp() {
parent::setUp();
// Manually include the feeds module.
// @todo Allow an array of modules from the child class.
drupal_load('module', 'feeds');
}
}
class FeedsUnitTestCase extends FeedsUnitTestHelper {
public static function getInfo() {
return array(
'name' => 'Unit tests',
'description' => 'Test basic low-level Feeds module functionality.',
'group' => 'Feeds',
);
}
/**
* Test valid absolute urls.
*
* @see ValidUrlTestCase
*
* @todo Remove when http://drupal.org/node/1191252 is fixed.
*/
function testFeedsValidURL() {
$url_schemes = array('http', 'https', 'ftp', 'feed', 'webcal');
$valid_absolute_urls = array(
'example.com',
'www.example.com',
'ex-ample.com',
'3xampl3.com',
'example.com/paren(the)sis',
'example.com/index.html#pagetop',
'example.com:8080',
'subdomain.example.com',
'example.com/index.php?q=node',
'example.com/index.php?q=node&param=false',
'user@www.example.com',
'user:pass@www.example.com:8080/login.php?do=login&style=%23#pagetop',
'127.0.0.1',
'example.org?',
'john%20doe:secret:foo@example.org/',
'example.org/~,$\'*;',
'caf%C3%A9.example.org',
'[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html',
'graph.asfdasdfasdf.com/blarg/feed?access_token=133283760145143|tGew8jbxi1ctfVlYh35CPYij1eE',
);
foreach ($url_schemes as $scheme) {
foreach ($valid_absolute_urls as $url) {
$test_url = $scheme . '://' . $url;
$valid_url = feeds_valid_url($test_url, TRUE);
$this->assertTrue($valid_url, t('@url is a valid url.', array('@url' => $test_url)));
}
}
$invalid_ablosule_urls = array(
'',
'ex!ample.com',
'ex%ample.com',
);
foreach ($url_schemes as $scheme) {
foreach ($invalid_ablosule_urls as $url) {
$test_url = $scheme . '://' . $url;
$valid_url = feeds_valid_url($test_url, TRUE);
$this->assertFalse($valid_url, t('@url is NOT a valid url.', array('@url' => $test_url)));
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,10 @@
Title,Body,published,GUID
"Ut wisi enim ad minim veniam", "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.",205200720,2
"Duis autem vel eum iriure dolor", "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.",428112720,3
"Nam liber tempor", "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.",1151766000,1
Typi non habent"", "Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem.",1256326995,4
"Lorem ipsum","Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",1251936720,1
"Investigationes demonstraverunt", "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",946702800,5
"Claritas est etiam", "Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum.",438112720,6
"Mirum est notare", "Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima.",1151066000,7
"Eodem modo typi", "Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.",1201936720,8
Can't render this file because it contains an unexpected character in line 5 and column 16.

View File

@@ -0,0 +1,3 @@
Title,Body,published,GUID
"Duis autem vel eum iriure dolor", "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.",428112720,3
"Nam liber tempor", "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.",1151766000,1
1 Title Body published GUID
2 Duis autem vel eum iriure dolor Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 428112720 3
3 Nam liber tempor Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. 1151766000 1

View File

@@ -0,0 +1,4 @@
Title,Body,published,GUID
"Typi non habent", "Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem.",1256326995,4
"Lorem ipsum","Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",1251936720,1
"Investigationes demonstraverunt", "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",946702800,5
1 Title Body published GUID
2 Typi non habent Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem. 1256326995 4
3 Lorem ipsum Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 1251936720 1
4 Investigationes demonstraverunt Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius. 946702800 5

View File

@@ -0,0 +1,3 @@
Title,Body,published,GUID
"Investigationes demonstraverunt", "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",946702800,5
"Claritas est etiam", "Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum.",438112720,6
1 Title Body published GUID
2 Investigationes demonstraverunt Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius. 946702800 5
3 Claritas est etiam Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum. 438112720 6

View File

@@ -0,0 +1,3 @@
Title,Body,published,GUID
"Mirum est notare", "Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima.",1151066000,7
"Eodem modo typi", "Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.",1201936720,8
1 Title Body published GUID
2 Mirum est notare Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima. 1151066000 7
3 Eodem modo typi Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum. 1201936720 8

View File

@@ -0,0 +1,3 @@
"guid","title","created","alpha","beta","gamma","delta","body"
1,"Lorem ipsum",1251936720,"Lorem",42,"4.2",3.14159265,"Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat."
2,"Ut wisi enim ad minim veniam",1251932360,"Ut wisi",32,"1.2",5.62951413,"Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat."
1 guid title created alpha beta gamma delta body
2 1 Lorem ipsum 1251936720 Lorem 42 4.2 3.14159265 Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
3 2 Ut wisi enim ad minim veniam 1251932360 Ut wisi 32 1.2 5.62951413 Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.

View File

@@ -0,0 +1,299 @@
<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xml:base="http://developmentseed.org/blog/all" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Development Seed - Technological Solutions for Progressive Organizations</title>
<link>http://developmentseed.org/blog/all</link>
<description></description>
<language>en</language>
<item>
<title>Open Atrium Translation Workflow: Two Way Translation Updates</title>
<link>http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;A new translation process for Open Atrium &amp; integration with Localize Drupal&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;The &lt;a href=&quot;http://openatrium.com/&quot;&gt;Open Atrium&lt;/a&gt; &lt;a href=&quot;http://developmentseed.org/blog/2009/jul/16/open-atrium-solving-translation-puzzle&quot;&gt;translation infrastructure&lt;/a&gt; (and Drupal translations in general) are progressing quickly. For Open Atrium to be well translated we first need Drupal&#039;s modules to be translated, so I am splitting efforts at the moment between helping with &lt;a href=&quot;http://localize.drupal.org&quot;&gt;Localize Drupal&lt;/a&gt; and improving &lt;a href=&quot;https://translate.openatrium.com&quot;&gt;Open Atrium Translate&lt;/a&gt;. Already, it is much easier to automatically download your language, get updates from a translation server, protect locally translated strings, and scale the translation system so that translation servers can talk to each other.&lt;/p&gt;
&lt;h1&gt;Automatically download your language&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2496/3984689117_57559c74eb.jpg&quot; alt=&quot;Magical translation install&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For more than a month now you have been able to install Open Atrium, select one of its 20+ languages, and have the translation automatically downloaded and installed on your site. While this has been working for awhile now, we are still refining the process. One change we&#039;ve already made is that now translations are downloaded in multiple smaller packages, rather than a single large one.&lt;/p&gt;
&lt;p&gt;Once your translation is installed, you can use tools like the &lt;a href=&quot;http://drupal.org/project/l10n_client&quot;&gt;Localization client&lt;/a&gt;, which comes bundled in the Open Atrium install, to translate page strings and then optionally contribute them back to the localization server, automatically. This flow of translations goes both ways, so your site gets the latest updates from the server just as you can send your latest updates to the server.&lt;/p&gt;
&lt;h1&gt;Two way translation updates&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;But what happens with my locally translated strings, which I like more than the ones that come out of the box, when I update from the server?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2442/3984689343_e9b7c32718.jpg&quot; alt=&quot;Two ways translation updates&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In a word, nothing. There has been a major improvement on this front. Now your translations are tracked and won&#039;t be overwritten by someone else&#039;s translations when you update, unless you choose for them to be. This means that you can contribute your translations, benefit from others contributing theirs, and make the world a better (translated) place, without loosing any custom work that you want. Let the translations flow!&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating#comments</comments>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/localization">localization</category>
<category domain="http://developmentseed.org/tags/localization-client">localization client</category>
<category domain="http://developmentseed.org/tags/localization-server">localization server</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/translation">translation</category>
<category domain="http://developmentseed.org/tags/translation-server">translation server</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Tue, 06 Oct 2009 15:21:48 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">974 at http://developmentseed.org</guid>
</item>
<item>
<title>Week in DC Tech: October 5th Edition</title>
<link>http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Drupal, PHP, and Mapping This Week in Washington, DC&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;img src=&quot;http://developmentseed.org/sites/developmentseed.org/files/dctech2_0_0.png&quot; alt=&quot;Week in DC Tech&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There are some great technology events happening this week in Washington, DC, so if you&#039;re looking to talk code, help map the city, or just hear about some neat projects and ideas, you&#039;re in luck. Below are the events that caught our eye, and you can find a full list of technology events happening this week at &lt;a href=&quot;http://www.dctechevents.com/&quot;&gt;DC Tech Events&lt;/a&gt;. Have a great week!&lt;/p&gt;
&lt;h1&gt;Wednesday, October 7&lt;/h1&gt;
&lt;p&gt;6:30 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://drupal.meetup.com/21/calendar/11332695/&quot;&gt;&lt;strong&gt;NOVA Drupal Meetup&lt;/strong&gt;&lt;/a&gt;: Are you a Drupal developer, use a Drupal site, or just want to learn more about the open source content management system? Come out for this meetup to meet other Drupal fans and hear how people are using the CMS.&lt;/p&gt;
&lt;p&gt;6:30 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://groups.google.com/group/washington-dcphp-group/browse_thread/thread/716d4a625287fef5?hl=en&quot;&gt;&lt;strong&gt;DC PHP Beverage Subgroup&lt;/strong&gt;&lt;/a&gt;: If you&#039;re looking to talk code with PHP developers who share your interest, come out for this casual meetup to chat over beers.&lt;/p&gt;
&lt;p&gt;7:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://hacdc.org/&quot;&gt;&lt;strong&gt;Mapping DC Meeting&lt;/strong&gt;&lt;/a&gt;: Come out for this meetup if you want to help create high quality, free maps of Washington, DC. The &lt;a href=&quot;http://wiki.openstreetmap.org/wiki/MappingDC&quot;&gt;Mapping DC&lt;/a&gt; group is doing this, starting with creating a detailed map of the National Zoo.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition#comments</comments>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Mon, 05 Oct 2009 15:27:40 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">973 at http://developmentseed.org</guid>
</item>
<item>
<title>Mapping Innovation at the World Bank with Open Atrium</title>
<link>http://developmentseed.org/blog/2009/oct/02/mapping-innovation-world-bank-open-atrium</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Using map and faceted search features to improve collaboration&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;a href=&quot;http://openatrium.com/&quot;&gt;Open Atrium&lt;/a&gt; is being used as a base platform for collaboration at the World Bank because of its feature flexibility. Last week the World Bank launched a new Open Atrium site called &quot;Innovate,&quot; which is being used to support an organization-wide initiative to better share information about successful projects and approaches to solving problems.&lt;/p&gt;
&lt;p&gt;The core of the site is built around helping World Bank staff discover relevant &quot;innovations&quot; happening around the world and providing a space to discuss them with colleagues in topical discussion groups. To facilitate this workflow we built a custom map-based browser feature that combines custom maps with faceted search, letting users quickly find interesting content. The screenshots below from a staging site with a partial database show what this feature looks like.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3419/3974644312_c992e1afe8.jpg&quot; alt=&quot;The map-based browser feature makes custom maps with faceted search&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As users apply new facets to their searches, the map results update to reveal global coverage for innovations that meet the search criteria.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2600/3974644162_a44cc3a89a.jpg&quot; alt=&quot;Add new facets to the search to further customize the map&quot; /&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/oct/02/mapping-innovation-world-bank-open-atrium#comments</comments>
<category domain="http://developmentseed.org/tags/custom-mapping">custom mapping</category>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/faceted-search">faceted search</category>
<category domain="http://developmentseed.org/tags/intranet">intranet</category>
<category domain="http://developmentseed.org/tags/map-basec-browser">map-basec browser</category>
<category domain="http://developmentseed.org/tags/mapbox">mapbox</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/world-bank">World Bank</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Fri, 02 Oct 2009 14:31:04 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">972 at http://developmentseed.org</guid>
</item>
<item>
<title>September GeoDC Meetup Tonight</title>
<link>http://developmentseed.org/blog/2009/sep/30/september-geodc-meetup-tonight</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Presentations on Using Amazon&amp;#8217;s Web Services and OpenStreet Map and an iPhone App that Maps Government Data&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;Today is the last Wednesday of the month, which means it&#039;s time for another &lt;a href=&quot;http://geo-dc.ning.com/xn/detail/3537548:Event:1223?xg_source=activity&quot;&gt;GeoDC meetup&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3525/3966592859_f7f4cb179c.jpg&quot; alt=&quot;September GeoDC Meetup&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There will be two short presentations at the meetup. &lt;a href=&quot;http://developmentseed.org/team/tom-macwright&quot;&gt;Tom MacWright&lt;/a&gt; from Development Seed will talk about how using Amazon&#039;s web services and &lt;a href=&quot;http://www.openstreetmap.org/&quot;&gt;OpenStreetMap&lt;/a&gt; has helped our mapping team design, render, and host custom maps. Brian Sobel of &lt;a href=&quot;http://www.innovationgeo.com/&quot;&gt;Innovation Geo&lt;/a&gt; will present &lt;a href=&quot;http://areyousafedc.com/&quot;&gt;Are You Safe&lt;/a&gt;, an iPhone App that uses open government data to give users up-to-date and hyper-local information about crime.&lt;/p&gt;
&lt;p&gt;The meetup will run from 7:00 to 9:00 pm at the offices of &lt;a href=&quot;http://www.fortiusone.com&quot;&gt;Fortius One&lt;/a&gt; at 2200 Wilson Blvd, Suite 307 in Arlington, just a &lt;a href=&quot;http://maps.google.com/maps?f=q&amp;amp;source=s_q&amp;amp;hl=en&amp;amp;geocode=&amp;amp;q=2200+Wilson+Blvd+%23+307+Arlington,+VA+22201-3324&amp;amp;sll=38.893037,-77.072783&amp;amp;sspn=0.039481,0.087633&amp;amp;ie=UTF8&amp;amp;ll=38.8912,-77.086236&amp;amp;spn=0.009871,0.021908&amp;amp;t=h&amp;amp;z=16&amp;amp;iwloc=A&quot;&gt;short walk from the Courthouse metro stop on the orange line&lt;/a&gt;. Hope to see you there!&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/30/september-geodc-meetup-tonight#comments</comments>
<category domain="http://developmentseed.org/tags/geodc">GeoDC</category>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Wed, 30 Sep 2009 12:02:53 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">971 at http://developmentseed.org</guid>
</item>
<item>
<title>Week in DC Tech: September 28th Edition</title>
<link>http://developmentseed.org/blog/2009/sep/28/week-dc-tech-september-28th-edition</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Healthcare 2.0, iPhone Development, Online Storytelling, and More This Week in Washington, DC&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;img src=&quot;http://developmentseed.org/sites/developmentseed.org/files/dctech2_0_0.png&quot; alt=&quot;Week in DC Tech&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Looking to geek out this week? There are a bunch of interesting technology events happening in Washington, DC this week, including a look at how social media is impacting healthcare, a screening of online clips from a journalist/filmmaker, and lightning talks on all kinds of geekery by the HacDC folks. Below are the events that caught our eye, and you can find a full list of what&#039;s happening this week in technology over at &lt;a href=&quot;http://www.dctechevents.com/&quot;&gt;DC Tech Events&lt;/a&gt;. Have a great week!&lt;/p&gt;
&lt;h1&gt;Tuesday, September 29&lt;/h1&gt;
&lt;p&gt;6:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.meetup.com/DC-MD-VA-Health-2-0/calendar/11291017/&quot;&gt;&lt;strong&gt;Health 2.0 Meetup&lt;/strong&gt;&lt;/a&gt;: Curious as to how - and if - online technologies are impacting healthcare? At this meetup two speakers - Sanjay Koyani from the U.S. Food and Drug Administration and Taylor Walsh from MetroHealth Media - will talk about what they&#039;re seeing and implementing.&lt;/p&gt;
&lt;p&gt;7:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://nscodernightdc.com/&quot;&gt;&lt;strong&gt;NSCoderNightDC&lt;/strong&gt;&lt;/a&gt;: Want to build an iphone app, or talk about one that you&#039;ve already built? Come out for this meetup to talk about mac and iphone development, share the latest news from Apple, and eat some delicious French desserts.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/28/week-dc-tech-september-28th-edition#comments</comments>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Mon, 28 Sep 2009 15:33:15 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">970 at http://developmentseed.org</guid>
</item>
<item>
<title>Open Data for Microfinance: The New MIXMarket.org</title>
<link>http://developmentseed.org/blog/2009/sep/24/open-data-microfinance-new-mixmarketorg</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Relaunch focuses on rich data visualization and downloadable data&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;The launch of the new &lt;a href=&quot;http://www.mixmarket.org/&quot;&gt;MIX Market&lt;/a&gt; is a big win for open data in international development, and it vastly improves how rich financial data sets can be accessed. The MIX Market is like a Bloomberg for microfinance, publishing data on more than 1,500 microfinance institutions (MFIs) in more than 190 countries and affecting 80,021,351 people. Additionally, each MFI has as many as 150 financial indicators and in some cases going back as far as 1995. The goal of this tool is simple - to open this information up to help MFIs, researchers, raters, evaluators, and governmental and regulatory agencies better see the marketplace, and that makes for better international development.&lt;/p&gt;
&lt;p&gt;There are profiles for every country that the MIX Market is hosting. Take a look at the country landing page for India, showing how India stacks up to other peer groups and listing out all MFIs, networks, and funders and service providers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3517/3941870722_390f5aa65d.jpg&quot; alt=&quot;Country profiles give a quick overview of the performance of its microfinance institutions.&quot; /&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/24/open-data-microfinance-new-mixmarketorg#comments</comments>
<category domain="http://developmentseed.org/tags/data-visualization">data visualization</category>
<category domain="http://developmentseed.org/tags/graphs">graphs</category>
<category domain="http://developmentseed.org/tags/microfinance">microfinance</category>
<category domain="http://developmentseed.org/tags/mix-market">MIX Market</category>
<category domain="http://developmentseed.org/tags/open-data">open data</category>
<category domain="http://developmentseed.org/tags/salesforce">salesforce</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Thu, 24 Sep 2009 13:09:10 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">969 at http://developmentseed.org</guid>
</item>
<item>
<title>Integrating the Siteminder Access System in an Open Atrium-based Intranet</title>
<link>http://developmentseed.org/blog/2009/sep/22/integrating-siteminder-access-system-open-atrium-based-intranet</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Upgraded Siteminder Module in Drupal allows for better integration with Siteminder &lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;In &lt;a href=&quot;http://developmentseed.org/blog/2009/sep/08/custom-open-atrium-intranet-launches-world-bank&quot;&gt;our recent work on the World Bank&#039;s Communicate intranet&lt;/a&gt;, we needed to integrate the &lt;a href=&quot;http://www.ca.com/us/internet-access-control.aspx&quot;&gt;Siteminder access system&lt;/a&gt; into the &lt;a href=&quot;http://openatrium.com/&quot;&gt;Open Atrium&lt;/a&gt;-based intranet &quot;Communicate&quot; to allow World Bank staff to use the same single sign-on credentials that they use to access all their internal web systems. To do this, we upgraded the Siteminder module for Drupal. You can download the &lt;a href=&quot;http://drupal.org/project/siteminder&quot;&gt;new module from its Drupal project page&lt;/a&gt; and &lt;a href=&quot;http://cvs.drupal.org/viewvc.py/drupal/contributions/modules/siteminder/README.txt?revision=1.2&amp;amp;view=markup&amp;amp;pathrev=DRUPAL-6--1-0-ALPHA1&quot;&gt;learn more about its API and how to write your own Siteminder plugin in its documentation&lt;/a&gt; and from reading the module&#039;s code. First, here is a little more background on the changes.&lt;/p&gt;
&lt;p&gt;The Siteminder system, from &lt;a href=&quot;http://www.ca.com/us/&quot;&gt;Computer Associates&lt;/a&gt;, is used by many enterprise-level organizations to authenticate signing on to their web resources. How it works is that you can designate a site - like an Open Atrium powered intranet - to be protected by the Siteminder system. Once a site is protected by Siteminder, all traffic to that site is routed through Siteminder first and then on to the actual site. Siteminder sets certain HTTP headers in the user&#039;s request, and Drupal can then examine them to determine credentials. What the Drupal Siteminder module does is map the Siteminder header values to Drupal users and allow a user to login based on the headers they send.&lt;/p&gt;
&lt;p&gt;In addition to authentication, the Siteminder system also stores other information about users. When the Siteminder system sends HTTP headers for authentication, it can also send information about a user - like her name, email address, phone number, and so on. We wanted to be able to pull this information into the intranet too. To achieve this, we re-wrote the Siteminder module in such a way that it&#039;s easy to write a plugin module to provide the fields to which you&#039;d like to map this extra Siteminder meta information and to determine how this information is processed and saved. To do this for the World Bank&#039;s intranet, we built the Siteminder Profile module, which lets you pick a CCK node type to serve as the target content profile for a user as well as select a few taxonomy vocabularies. Then by using the main module&#039;s administrative interface, you can choose which Siteminder headers should get mapped to which CCK fields and vocabularies based on the designated node type and vocabularies you selected in the Siteminder Profile settings page.&lt;/p&gt;
&lt;p&gt;But what happens if a person&#039;s information changes in the Siteminder database - for example if they change phone numbers or office buildings? The Siteminder module now has built-in capability and an API to check whether values in users&#039; profiles have changed in the Siteminder system. The Siteminder Profile module uses this API and saves a new version of a user&#039;s profile if it detects that a value has changed in the Siteminder system database.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/22/integrating-siteminder-access-system-open-atrium-based-intranet#comments</comments>
<category domain="http://developmentseed.org/tags/authentication">authentication</category>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/siteminder">siteminder</category>
<category domain="http://developmentseed.org/tags/siteminder-module">siteminder module</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Tue, 22 Sep 2009 18:02:21 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">964 at http://developmentseed.org</guid>
</item>
<item>
<title>Week in DC Tech: September 21 Edition</title>
<link>http://developmentseed.org/blog/2009/sep/21/week-dc-tech-september-21-edition</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;PHP, Design, Twitter, and Wikipedia This Week in Washington, DC&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;img src=&quot;http://developmentseed.org/sites/default/files/dctech2_0.png&quot; alt=&quot;Week in DC Tech&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There&#039;s an interesting variety of technology events happening in Washington, DC this week with focuses ranging from using Twitter for advocacy to drinking beers with php developers to discussing designing way outside of the box. Additionally tomorrow is international Car Free Day and there are events happening throughout the city to celebrate it and help you how to rely on cars less. Below are the events that caught our eye, and you can find a full list of the week&#039;s technology events at &lt;a href=&quot;http://www.dctechevents.com/&quot;&gt;DC Tech Events&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Tuesday, September 22&lt;/h2&gt;
&lt;p&gt;All day&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.carfreemetrodc.com/&quot;&gt;&lt;strong&gt;Car Free Day&lt;/strong&gt;&lt;/a&gt;: Help reduce traffic and improve air quality by leaving your car at home on Tuesday in celebration of Car Free Day. There are also &lt;a href=&quot;http://www.carfreemetrodc.com/Information/tabid/57/Default.aspx&quot;&gt;free bike repair trainings, yoga classes, and other events&lt;/a&gt; happening throughout the day to help you lead a car free lifestyle.&lt;/p&gt;
&lt;p&gt;6:00 - 8:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.php.net/cal.php?id=3075&quot;&gt;&lt;strong&gt;DC PHP Beverage Subgroup&lt;/strong&gt;&lt;/a&gt;: Come out to talk code with other php developers over a few beers. This is a great opportunity to get to know local php developers in a casual setting while sharing stories about your code.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/21/week-dc-tech-september-21-edition#comments</comments>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Mon, 21 Sep 2009 16:03:24 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">968 at http://developmentseed.org</guid>
</item>
<item>
<title>Peru&#039;s Software Freedom Day: Impressions &amp; Photos</title>
<link>http://developmentseed.org/blog/2009/sep/21/perus-software-freedom-day-impressions-and-photos</link>
<description>&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;There was a great turn out a &lt;a href=&quot;http://www.sfdperu.org/&quot;&gt;Software Freedom Day&lt;/a&gt; this weekend with 400 people in attendance and a solid 30 presentations. The &lt;a href=&quot;http://developmentseed.org/blog/2009/sep/15/preparing-perus-software-freedom-day-talks-drupal-features-and-open-atrium&quot;&gt;presentations in the Drupal track&lt;/a&gt; were some of the best attended sessions of the day. To get a sense of Drupal&#039;s traction down here, &quot;Drupal&quot; was mentioned in many sessions and conversations throughout the day, and not just by the people working directly with Drupal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2653/3940335249_57ce995a84.jpg&quot; alt=&quot;Presenting on Features in Drupal and Open Atrium&quot; /&gt;
&lt;em&gt;Presenting on Features in Drupal and Open Atrium&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I had a great time meeting people and learning about the work being done in the different open source communities here in Peru. Software Freedom Day is becoming an annual event in Peru, and there were many discussions on improving the event for next year as well as keeping the energy going to improve the image of open source software in the country. It&#039;s great to see the community looking forward like this, and I&#039;m excited to help keep the open source movement growing in Peru.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/developmentseed/sets/72157622423999830/&quot;&gt;More photos from the event here.&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/21/perus-software-freedom-day-impressions-and-photos#comments</comments>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/open-source">open source</category>
<category domain="http://developmentseed.org/tags/peru">Peru</category>
<category domain="http://developmentseed.org/tags/software-freedom-day">software freedom day</category>
<pubDate>Mon, 21 Sep 2009 14:22:35 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">967 at http://developmentseed.org</guid>
</item>
<item>
<title>Scaling the Open Atrium UI</title>
<link>http://developmentseed.org/blog/2009/sep/18/scaling-open-atrium-ui</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Refactoring a user interface for bigger, broader use cases&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;We released &lt;a href=&quot;http://developmentseed.org/blog/2009/jul/14/open-atrium-public-beta-code-github&quot;&gt;Open Atrium Beta 1&lt;/a&gt; in July knowing that a wider audience would lead to more real world testing, feedback, and problems. In &lt;a href=&quot;http://openatrium.com/download&quot;&gt;the latest release this week&lt;/a&gt;, we&#039;ve incorporated some of the responses we&#039;ve gotten from the &lt;a href=&quot;http://community.openatrium.com&quot;&gt;community&lt;/a&gt;, as well as our &lt;a href=&quot;http://developmentseed.org/blog/2009/sep/08/custom-open-atrium-intranet-launches-world-bank&quot;&gt;clients&#039; experiences&lt;/a&gt; into key changes in Open Atrium&#039;s UI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2607/3928674475_285044e13c.jpg&quot; alt=&quot; Fluid width/ Breadcrumbs&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Fluid width&lt;/h2&gt;
&lt;p&gt;The first major change is switching from the previously &lt;code&gt;960px&lt;/code&gt; fixed width theme Ginkgo to fluid width. Open Atrium is now usable on screens from &lt;code&gt;800px&lt;/code&gt; wide to as-big-as-your-budget-allows. Aside from meaning that the page layout stretches and shrinks when you resize your browser window, it also means slightly bigger/more readable fonts overall.&lt;/p&gt;
&lt;h2&gt;1. Breadcrumbs&lt;/h2&gt;
&lt;p&gt;One usability problem we often deal with is people not recognizing the site / group / user space architecture of Open Atrium. One approach to this in the past was to color-code different space types. With the color-customizations made possible by &lt;code&gt;spaces_design&lt;/code&gt;, this is no longer necessarily a reliable indicator of where you are. We first introduced breadcrumbs in Open Atrium on the Communicate project for the World Bank and have since merged it into Atrium HEAD.&lt;/p&gt;
&lt;h2&gt;2. Consolidate blocks, user info, and help&lt;/h2&gt;
&lt;p&gt;The global tools in Open Atrium&#039;s first row of navigation have been consolidated into distinct blocks. Previously, some header links were inserted into the page template through custom &lt;code&gt;preprocess_page()&lt;/code&gt; calls, while the togglable help text was inserted via a custom theme function and dropdown blocks were added into the header region. All of these components are now provided by blocks, making it straightforward for themers and developers to adjust, remove, or add to these components.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2654/3928674449_8f443df1b3.jpg&quot; alt=&quot;Togglable Open Atrium UI adjustments&quot; /&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/18/scaling-open-atrium-ui#comments</comments>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/interface">interface</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/usability">usability</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Fri, 18 Sep 2009 14:31:23 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">966 at http://developmentseed.org</guid>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,299 @@
<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xml:base="http://developmentseed.org/blog/all" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Development Seed - Technological Solutions for Progressive Organizations</title>
<link>http://developmentseed.org/blog/all</link>
<description></description>
<language>en</language>
<item>
<title>Managing News Translation Workflow: Two Way Translation Updates</title>
<link>http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;A new translation process for Open Atrium and integration with Localize Drupal&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;The &lt;a href=&quot;http://openatrium.com/&quot;&gt;Open Atrium&lt;/a&gt; &lt;a href=&quot;http://developmentseed.org/blog/2009/jul/16/open-atrium-solving-translation-puzzle&quot;&gt;translation infrastructure&lt;/a&gt; (and Drupal translations in general) are progressing quickly. For Open Atrium to be well translated we first need Drupal&#039;s modules to be translated, so I am splitting efforts at the moment between helping with &lt;a href=&quot;http://localize.drupal.org&quot;&gt;Localize Drupal&lt;/a&gt; and improving &lt;a href=&quot;https://translate.openatrium.com&quot;&gt;Open Atrium Translate&lt;/a&gt;. Already, it is much easier to automatically download your language, get updates from a translation server, protect locally translated strings, and scale the translation system so that translation servers can talk to each other.&lt;/p&gt;
&lt;h1&gt;Automatically download your language&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2496/3984689117_57559c74eb.jpg&quot; alt=&quot;Magical translation install&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For more than a month now you have been able to install Open Atrium, select one of its 20+ languages, and have the translation automatically downloaded and installed on your site. While this has been working for awhile now, we are still refining the process. One change we&#039;ve already made is that now translations are downloaded in multiple smaller packages, rather than a single large one.&lt;/p&gt;
&lt;p&gt;Once your translation is installed, you can use tools like the &lt;a href=&quot;http://drupal.org/project/l10n_client&quot;&gt;Localization client&lt;/a&gt;, which comes bundled in the Open Atrium install, to translate page strings and then optionally contribute them back to the localization server, automatically. This flow of translations goes both ways, so your site gets the latest updates from the server just as you can send your latest updates to the server.&lt;/p&gt;
&lt;h1&gt;Two way translation updates&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;But what happens with my locally translated strings, which I like more than the ones that come out of the box, when I update from the server?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2442/3984689343_e9b7c32718.jpg&quot; alt=&quot;Two ways translation updates&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In a word, nothing. There has been a major improvement on this front. Now your translations are tracked and won&#039;t be overwritten by someone else&#039;s translations when you update, unless you choose for them to be. This means that you can contribute your translations, benefit from others contributing theirs, and make the world a better (translated) place, without loosing any custom work that you want. Let the translations flow!&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating#comments</comments>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/localization">localization</category>
<category domain="http://developmentseed.org/tags/localization-client">localization client</category>
<category domain="http://developmentseed.org/tags/localization-server">localization server</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/translation">translation</category>
<category domain="http://developmentseed.org/tags/translation-server">translation server</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Tue, 06 Oct 2009 15:21:48 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">974 at http://developmentseed.org</guid>
</item>
<item>
<title>Week in DC Tech: October 5th Edition</title>
<link>http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Drupal, PHP, and Mapping This Week in Washington, DC&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;img src=&quot;http://developmentseed.org/sites/developmentseed.org/files/dctech2_0_0.png&quot; alt=&quot;Week in DC Tech&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There are some great technology events happening this week in Washington, DC, so if you&#039;re looking to talk code, help map the city, or just hear about some neat projects and ideas, you&#039;re in luck. Below are the events that caught our eye, and you can find a full list of technology events happening this week at &lt;a href=&quot;http://www.dctechevents.com/&quot;&gt;DC Tech Events&lt;/a&gt;. Have a great week!&lt;/p&gt;
&lt;h1&gt;Wednesday, October 7&lt;/h1&gt;
&lt;p&gt;6:30 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://drupal.meetup.com/21/calendar/11332695/&quot;&gt;&lt;strong&gt;NOVA Drupal Meetup&lt;/strong&gt;&lt;/a&gt;: Are you a Drupal developer, use a Drupal site, or just want to learn more about the open source content management system? Come out for this meetup to meet other Drupal fans and hear how people are using the CMS.&lt;/p&gt;
&lt;p&gt;6:30 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://groups.google.com/group/washington-dcphp-group/browse_thread/thread/716d4a625287fef5?hl=en&quot;&gt;&lt;strong&gt;DC PHP Beverage Subgroup&lt;/strong&gt;&lt;/a&gt;: If you&#039;re looking to talk code with PHP developers who share your interest, come out for this casual meetup to chat over beers.&lt;/p&gt;
&lt;p&gt;7:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://hacdc.org/&quot;&gt;&lt;strong&gt;Mapping DC Meeting&lt;/strong&gt;&lt;/a&gt;: Come out for this meetup if you want to help create high quality, free maps of Washington, DC. The &lt;a href=&quot;http://wiki.openstreetmap.org/wiki/MappingDC&quot;&gt;Mapping DC&lt;/a&gt; group is doing this, starting with creating a detailed map of the National Zoo.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/oct/05/week-dc-tech-october-5th-edition#comments</comments>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Mon, 05 Oct 2009 15:27:40 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">973 at http://developmentseed.org</guid>
</item>
<item>
<title>Mapping Innovation at the World Bank with Open Atrium</title>
<link>http://developmentseed.org/blog/2009/oct/02/mapping-innovation-world-bank-open-atrium</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Using map and faceted search features to improve collaboration&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;a href=&quot;http://openatrium.com/&quot;&gt;Open Atrium&lt;/a&gt; is being used as a base platform for collaboration at the World Bank because of its feature flexibility. Last week the World Bank launched a new Open Atrium site called &quot;Innovate,&quot; which is being used to support an organization-wide initiative to better share information about successful projects and approaches to solving problems.&lt;/p&gt;
&lt;p&gt;The core of the site is built around helping World Bank staff discover relevant &quot;innovations&quot; happening around the world and providing a space to discuss them with colleagues in topical discussion groups. To facilitate this workflow we built a custom map-based browser feature that combines custom maps with faceted search, letting users quickly find interesting content. The screenshots below from a staging site with a partial database show what this feature looks like.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3419/3974644312_c992e1afe8.jpg&quot; alt=&quot;The map-based browser feature makes custom maps with faceted search&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As users apply new facets to their searches, the map results update to reveal global coverage for innovations that meet the search criteria.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2600/3974644162_a44cc3a89a.jpg&quot; alt=&quot;Add new facets to the search to further customize the map&quot; /&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/oct/02/mapping-innovation-world-bank-open-atrium#comments</comments>
<category domain="http://developmentseed.org/tags/custom-mapping">custom mapping</category>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/faceted-search">faceted search</category>
<category domain="http://developmentseed.org/tags/intranet">intranet</category>
<category domain="http://developmentseed.org/tags/map-basec-browser">map-basec browser</category>
<category domain="http://developmentseed.org/tags/mapbox">mapbox</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/world-bank">World Bank</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Fri, 02 Oct 2009 14:31:04 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">972 at http://developmentseed.org</guid>
</item>
<item>
<title>September GeoDC Meetup Tonight</title>
<link>http://developmentseed.org/blog/2009/sep/30/september-geodc-meetup-tonight</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Presentations on Using Amazon&amp;#8217;s Web Services and OpenStreet Map and an iPhone App that Maps Government Data&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;Today is the last Wednesday of the month, which means it&#039;s time for another &lt;a href=&quot;http://geo-dc.ning.com/xn/detail/3537548:Event:1223?xg_source=activity&quot;&gt;GeoDC meetup&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3525/3966592859_f7f4cb179c.jpg&quot; alt=&quot;September GeoDC Meetup&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There will be two short presentations at the meetup. &lt;a href=&quot;http://developmentseed.org/team/tom-macwright&quot;&gt;Tom MacWright&lt;/a&gt; from Development Seed will talk about how using Amazon&#039;s web services and &lt;a href=&quot;http://www.openstreetmap.org/&quot;&gt;OpenStreetMap&lt;/a&gt; has helped our mapping team design, render, and host custom maps. Brian Sobel of &lt;a href=&quot;http://www.innovationgeo.com/&quot;&gt;Innovation Geo&lt;/a&gt; will present &lt;a href=&quot;http://areyousafedc.com/&quot;&gt;Are You Safe&lt;/a&gt;, an iPhone App that uses open government data to give users up-to-date and hyper-local information about crime.&lt;/p&gt;
&lt;p&gt;The meetup will run from 7:00 to 9:00 pm at the offices of &lt;a href=&quot;http://www.fortiusone.com&quot;&gt;Fortius One&lt;/a&gt; at 2200 Wilson Blvd, Suite 307 in Arlington, just a &lt;a href=&quot;http://maps.google.com/maps?f=q&amp;amp;source=s_q&amp;amp;hl=en&amp;amp;geocode=&amp;amp;q=2200+Wilson+Blvd+%23+307+Arlington,+VA+22201-3324&amp;amp;sll=38.893037,-77.072783&amp;amp;sspn=0.039481,0.087633&amp;amp;ie=UTF8&amp;amp;ll=38.8912,-77.086236&amp;amp;spn=0.009871,0.021908&amp;amp;t=h&amp;amp;z=16&amp;amp;iwloc=A&quot;&gt;short walk from the Courthouse metro stop on the orange line&lt;/a&gt;. Hope to see you there!&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/30/september-geodc-meetup-tonight#comments</comments>
<category domain="http://developmentseed.org/tags/geodc">GeoDC</category>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Wed, 30 Sep 2009 12:02:53 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">971 at http://developmentseed.org</guid>
</item>
<item>
<title>Week in DC Tech: September 28th Edition</title>
<link>http://developmentseed.org/blog/2009/sep/28/week-dc-tech-september-28th-edition</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Healthcare 2.0, iPhone Development, Online Storytelling, and More This Week in Washington, DC&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;img src=&quot;http://developmentseed.org/sites/developmentseed.org/files/dctech2_0_0.png&quot; alt=&quot;Week in DC Tech&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Looking to geek out this week? There are a bunch of interesting technology events happening in Washington, DC this week, including a look at how social media is impacting healthcare, a screening of online clips from a journalist/filmmaker, and lightning talks on all kinds of geekery by the HacDC folks. Below are the events that caught our eye, and you can find a full list of what&#039;s happening this week in technology over at &lt;a href=&quot;http://www.dctechevents.com/&quot;&gt;DC Tech Events&lt;/a&gt;. Have a great week!&lt;/p&gt;
&lt;h1&gt;Tuesday, September 29&lt;/h1&gt;
&lt;p&gt;6:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.meetup.com/DC-MD-VA-Health-2-0/calendar/11291017/&quot;&gt;&lt;strong&gt;Health 2.0 Meetup&lt;/strong&gt;&lt;/a&gt;: Curious as to how - and if - online technologies are impacting healthcare? At this meetup two speakers - Sanjay Koyani from the U.S. Food and Drug Administration and Taylor Walsh from MetroHealth Media - will talk about what they&#039;re seeing and implementing.&lt;/p&gt;
&lt;p&gt;7:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://nscodernightdc.com/&quot;&gt;&lt;strong&gt;NSCoderNightDC&lt;/strong&gt;&lt;/a&gt;: Want to build an iphone app, or talk about one that you&#039;ve already built? Come out for this meetup to talk about mac and iphone development, share the latest news from Apple, and eat some delicious French desserts.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/28/week-dc-tech-september-28th-edition#comments</comments>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Mon, 28 Sep 2009 15:33:15 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">970 at http://developmentseed.org</guid>
</item>
<item>
<title>Open Data for Microfinance: The New MIXMarket.org</title>
<link>http://developmentseed.org/blog/2009/sep/24/open-data-microfinance-new-mixmarketorg</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Relaunch focuses on rich data visualization and downloadable data&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;The launch of the new &lt;a href=&quot;http://www.mixmarket.org/&quot;&gt;MIX Market&lt;/a&gt; is a big win for open data in international development, and it vastly improves how rich financial data sets can be accessed. The MIX Market is like a Bloomberg for microfinance, publishing data on more than 1,500 microfinance institutions (MFIs) in more than 190 countries and affecting 80,021,351 people. Additionally, each MFI has as many as 150 financial indicators and in some cases going back as far as 1995. The goal of this tool is simple - to open this information up to help MFIs, researchers, raters, evaluators, and governmental and regulatory agencies better see the marketplace, and that makes for better international development.&lt;/p&gt;
&lt;p&gt;There are profiles for every country that the MIX Market is hosting. Take a look at the country landing page for India, showing how India stacks up to other peer groups and listing out all MFIs, networks, and funders and service providers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3517/3941870722_390f5aa65d.jpg&quot; alt=&quot;Country profiles give a quick overview of the performance of its microfinance institutions.&quot; /&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/24/open-data-microfinance-new-mixmarketorg#comments</comments>
<category domain="http://developmentseed.org/tags/data-visualization">data visualization</category>
<category domain="http://developmentseed.org/tags/graphs">graphs</category>
<category domain="http://developmentseed.org/tags/microfinance">microfinance</category>
<category domain="http://developmentseed.org/tags/mix-market">MIX Market</category>
<category domain="http://developmentseed.org/tags/open-data">open data</category>
<category domain="http://developmentseed.org/tags/salesforce">salesforce</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Thu, 24 Sep 2009 13:09:10 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">969 at http://developmentseed.org</guid>
</item>
<item>
<title>Integrating the Siteminder Access System in an Open Atrium-based Intranet</title>
<link>http://developmentseed.org/blog/2009/sep/22/integrating-siteminder-access-system-open-atrium-based-intranet</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Upgraded Siteminder Module in Drupal allows for better integration with Siteminder &lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;In &lt;a href=&quot;http://developmentseed.org/blog/2009/sep/08/custom-open-atrium-intranet-launches-world-bank&quot;&gt;our recent work on the World Bank&#039;s Communicate intranet&lt;/a&gt;, we needed to integrate the &lt;a href=&quot;http://www.ca.com/us/internet-access-control.aspx&quot;&gt;Siteminder access system&lt;/a&gt; into the &lt;a href=&quot;http://openatrium.com/&quot;&gt;Open Atrium&lt;/a&gt;-based intranet &quot;Communicate&quot; to allow World Bank staff to use the same single sign-on credentials that they use to access all their internal web systems. To do this, we upgraded the Siteminder module for Drupal. You can download the &lt;a href=&quot;http://drupal.org/project/siteminder&quot;&gt;new module from its Drupal project page&lt;/a&gt; and &lt;a href=&quot;http://cvs.drupal.org/viewvc.py/drupal/contributions/modules/siteminder/README.txt?revision=1.2&amp;amp;view=markup&amp;amp;pathrev=DRUPAL-6--1-0-ALPHA1&quot;&gt;learn more about its API and how to write your own Siteminder plugin in its documentation&lt;/a&gt; and from reading the module&#039;s code. First, here is a little more background on the changes.&lt;/p&gt;
&lt;p&gt;The Siteminder system, from &lt;a href=&quot;http://www.ca.com/us/&quot;&gt;Computer Associates&lt;/a&gt;, is used by many enterprise-level organizations to authenticate signing on to their web resources. How it works is that you can designate a site - like an Open Atrium powered intranet - to be protected by the Siteminder system. Once a site is protected by Siteminder, all traffic to that site is routed through Siteminder first and then on to the actual site. Siteminder sets certain HTTP headers in the user&#039;s request, and Drupal can then examine them to determine credentials. What the Drupal Siteminder module does is map the Siteminder header values to Drupal users and allow a user to login based on the headers they send.&lt;/p&gt;
&lt;p&gt;In addition to authentication, the Siteminder system also stores other information about users. When the Siteminder system sends HTTP headers for authentication, it can also send information about a user - like her name, email address, phone number, and so on. We wanted to be able to pull this information into the intranet too. To achieve this, we re-wrote the Siteminder module in such a way that it&#039;s easy to write a plugin module to provide the fields to which you&#039;d like to map this extra Siteminder meta information and to determine how this information is processed and saved. To do this for the World Bank&#039;s intranet, we built the Siteminder Profile module, which lets you pick a CCK node type to serve as the target content profile for a user as well as select a few taxonomy vocabularies. Then by using the main module&#039;s administrative interface, you can choose which Siteminder headers should get mapped to which CCK fields and vocabularies based on the designated node type and vocabularies you selected in the Siteminder Profile settings page.&lt;/p&gt;
&lt;p&gt;But what happens if a person&#039;s information changes in the Siteminder database - for example if they change phone numbers or office buildings? The Siteminder module now has built-in capability and an API to check whether values in users&#039; profiles have changed in the Siteminder system. The Siteminder Profile module uses this API and saves a new version of a user&#039;s profile if it detects that a value has changed in the Siteminder system database.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/22/integrating-siteminder-access-system-open-atrium-based-intranet#comments</comments>
<category domain="http://developmentseed.org/tags/authentication">authentication</category>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/siteminder">siteminder</category>
<category domain="http://developmentseed.org/tags/siteminder-module">siteminder module</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Tue, 22 Sep 2009 18:02:21 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">964 at http://developmentseed.org</guid>
</item>
<item>
<title>Week in DC Tech: September 21 Edition</title>
<link>http://developmentseed.org/blog/2009/sep/21/week-dc-tech-september-21-edition</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;PHP, Design, Twitter, and Wikipedia This Week in Washington, DC&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;&lt;img src=&quot;http://developmentseed.org/sites/default/files/dctech2_0.png&quot; alt=&quot;Week in DC Tech&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There&#039;s an interesting variety of technology events happening in Washington, DC this week with focuses ranging from using Twitter for advocacy to drinking beers with php developers to discussing designing way outside of the box. Additionally tomorrow is international Car Free Day and there are events happening throughout the city to celebrate it and help you how to rely on cars less. Below are the events that caught our eye, and you can find a full list of the week&#039;s technology events at &lt;a href=&quot;http://www.dctechevents.com/&quot;&gt;DC Tech Events&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Tuesday, September 22&lt;/h2&gt;
&lt;p&gt;All day&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.carfreemetrodc.com/&quot;&gt;&lt;strong&gt;Car Free Day&lt;/strong&gt;&lt;/a&gt;: Help reduce traffic and improve air quality by leaving your car at home on Tuesday in celebration of Car Free Day. There are also &lt;a href=&quot;http://www.carfreemetrodc.com/Information/tabid/57/Default.aspx&quot;&gt;free bike repair trainings, yoga classes, and other events&lt;/a&gt; happening throughout the day to help you lead a car free lifestyle.&lt;/p&gt;
&lt;p&gt;6:00 - 8:00 pm&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.php.net/cal.php?id=3075&quot;&gt;&lt;strong&gt;DC PHP Beverage Subgroup&lt;/strong&gt;&lt;/a&gt;: Come out to talk code with other php developers over a few beers. This is a great opportunity to get to know local php developers in a casual setting while sharing stories about your code.&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/21/week-dc-tech-september-21-edition#comments</comments>
<category domain="http://developmentseed.org/tags/washington-dc">Washington DC</category>
<pubDate>Mon, 21 Sep 2009 16:03:24 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">968 at http://developmentseed.org</guid>
</item>
<item>
<title>Peru&#039;s Software Freedom Day: Impressions and Photos</title>
<link>http://developmentseed.org/blog/2009/sep/21/perus-software-freedom-day-impressions-and-photos</link>
<description>&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;There was a great turn out a &lt;a href=&quot;http://www.sfdperu.org/&quot;&gt;Software Freedom Day&lt;/a&gt; this weekend with 400 people in attendance and a solid 30 presentations. The &lt;a href=&quot;http://developmentseed.org/blog/2009/sep/15/preparing-perus-software-freedom-day-talks-drupal-features-and-open-atrium&quot;&gt;presentations in the Drupal track&lt;/a&gt; were some of the best attended sessions of the day. To get a sense of Drupal&#039;s traction down here, &quot;Drupal&quot; was mentioned in many sessions and conversations throughout the day, and not just by the people working directly with Drupal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2653/3940335249_57ce995a84.jpg&quot; alt=&quot;Presenting on Features in Drupal and Managing News&quot; /&gt;
&lt;em&gt;Presenting on Features in Drupal and Managing News&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I had a great time meeting people and learning about the work being done in the different open source communities here in Peru. Software Freedom Day is becoming an annual event in Peru, and there were many discussions on improving the event for next year as well as keeping the energy going to improve the image of open source software in the country. It&#039;s great to see the community looking forward like this, and I&#039;m excited to help keep the open source movement growing in Peru.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/developmentseed/sets/72157622423999830/&quot;&gt;More photos from the event here.&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/21/perus-software-freedom-day-impressions-and-photos#comments</comments>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/open-source">open source</category>
<category domain="http://developmentseed.org/tags/peru">Peru</category>
<category domain="http://developmentseed.org/tags/software-freedom-day">software freedom day</category>
<pubDate>Mon, 21 Sep 2009 14:22:35 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">967 at http://developmentseed.org</guid>
</item>
<item>
<title>Scaling the Open Atrium UI</title>
<link>http://developmentseed.org/blog/2009/sep/18/scaling-open-atrium-ui</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;Refactoring a user interface for bigger, broader use cases&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;We released &lt;a href=&quot;http://developmentseed.org/blog/2009/jul/14/open-atrium-public-beta-code-github&quot;&gt;Open Atrium Beta 1&lt;/a&gt; in July knowing that a wider audience would lead to more real world testing, feedback, and problems. In &lt;a href=&quot;http://openatrium.com/download&quot;&gt;the latest release this week&lt;/a&gt;, we&#039;ve incorporated some of the responses we&#039;ve gotten from the &lt;a href=&quot;http://community.openatrium.com&quot;&gt;community&lt;/a&gt;, as well as our &lt;a href=&quot;http://developmentseed.org/blog/2009/sep/08/custom-open-atrium-intranet-launches-world-bank&quot;&gt;clients&#039; experiences&lt;/a&gt; into key changes in Open Atrium&#039;s UI.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2607/3928674475_285044e13c.jpg&quot; alt=&quot; Fluid width/ Breadcrumbs&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Fluid width&lt;/h2&gt;
&lt;p&gt;The first major change is switching from the previously &lt;code&gt;960px&lt;/code&gt; fixed width theme Ginkgo to fluid width. Open Atrium is now usable on screens from &lt;code&gt;800px&lt;/code&gt; wide to as-big-as-your-budget-allows. Aside from meaning that the page layout stretches and shrinks when you resize your browser window, it also means slightly bigger/more readable fonts overall.&lt;/p&gt;
&lt;h2&gt;1. Breadcrumbs&lt;/h2&gt;
&lt;p&gt;One usability problem we often deal with is people not recognizing the site / group / user space architecture of Open Atrium. One approach to this in the past was to color-code different space types. With the color-customizations made possible by &lt;code&gt;spaces_design&lt;/code&gt;, this is no longer necessarily a reliable indicator of where you are. We first introduced breadcrumbs in Open Atrium on the Communicate project for the World Bank and have since merged it into Atrium HEAD.&lt;/p&gt;
&lt;h2&gt;2. Consolidate blocks, user info, and help&lt;/h2&gt;
&lt;p&gt;The global tools in Open Atrium&#039;s first row of navigation have been consolidated into distinct blocks. Previously, some header links were inserted into the page template through custom &lt;code&gt;preprocess_page()&lt;/code&gt; calls, while the togglable help text was inserted via a custom theme function and dropdown blocks were added into the header region. All of these components are now provided by blocks, making it straightforward for themers and developers to adjust, remove, or add to these components.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2654/3928674449_8f443df1b3.jpg&quot; alt=&quot;Togglable Open Atrium UI adjustments&quot; /&gt;&lt;/p&gt;&lt;/div&gt;</description>
<comments>http://developmentseed.org/blog/2009/sep/18/scaling-open-atrium-ui#comments</comments>
<category domain="http://developmentseed.org/tags/drupal">Drupal</category>
<category domain="http://developmentseed.org/tags/interface">interface</category>
<category domain="http://developmentseed.org/tags/open-atrium">open atrium</category>
<category domain="http://developmentseed.org/tags/usability">usability</category>
<category domain="http://developmentseed.org/channel/drupal-planet">Drupal planet</category>
<pubDate>Fri, 18 Sep 2009 14:31:23 +0000</pubDate>
<dc:creator>Development Seed</dc:creator>
<guid isPermaLink="false">966 at http://developmentseed.org</guid>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,295 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>drupal.org aggregator</title>
<link>http://drupal.org/planet</link>
<description>drupal.org - aggregated feeds in category Planet Drupal</description>
<language>en</language>
<item>
<title>Adaptivethemes: Why I killed Node, may it RIP</title>
<link>http://adaptivethemes.com/why-i-killed-node-may-it-rip</link>
<description>&lt;p&gt;Myself, like many others, have always had an acrimonious relationship with the word &amp;#8220;node&amp;#8221;. It didn&amp;#8217;t exactly get off to a good start when node presented me with a rude &amp;#8220;wtf&amp;#8221; moment when we first met. Things only went down hill after that, node remaining aloof and abstract, without ever just coming out and telling me what it actually&amp;nbsp;was.&lt;/p&gt;
&lt;div class=&quot;og_rss_groups&quot;&gt;&lt;/div&gt;</description>
<pubDate>Fri, 23 Oct 2009 17:00:46 +0000</pubDate>
</item>
<item>
<title>Midwestern Mac, LLC: Managing News - Revolutionary—not Evolutionary—Step for Drupal</title>
<link>http://www.midwesternmac.com/blogs/geerlingguy/managing-news-revolutionary%E2%80%94not-evolutionary%E2%80%94step-drupal</link>
<description>&lt;p&gt;I noticed a post from the excellent folks over at &lt;a href=&quot;http://developmentseed.org/&quot;&gt;Development Seed&lt;/a&gt; in the drupal.org Planet feed on a new Drupal installation profile they&#039;ve been working on called &lt;a href=&quot;http://managingnews.com/&quot;&gt;Managing News&lt;/a&gt;. Having tried (and loved) their Drupal-based installation of &lt;a href=&quot;http://openatrium.com/&quot;&gt;Open Atrium&lt;/a&gt; (a great package for quick Intranets), I had pretty high expectations.&lt;/p&gt;
&lt;p&gt;Those expectations were pretty much blown out of the water; this install profile basically sets up a Drupal site (with all the Drupal bells and whistles) that is focused on one thing, and does it well: &lt;strong&gt;news aggregation via feeds&lt;/strong&gt; (Atom, RSS).&lt;/p&gt;
&lt;p class=&quot;rtecenter&quot;&gt;&lt;a href=&quot;http://catholicnewslive.com/&quot;&gt;&lt;img alt=&quot;Catholic News Live.com - Catholic News Aggregator&quot; width=&quot;450&quot; height=&quot;351&quot; class=&quot;noborder&quot; src=&quot;http://www.midwesternmac.com/sites/default/files/blogpost-images/catholic-news-live-screenshot.jpg&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I decided to quickly build out an aggregation site, &lt;a href=&quot;http://catholicnewslive.com/&quot;&gt;Catholic News Live&lt;/a&gt;. The site took about 4 hours to set up, and it&#039;s already relatively customized to my needs. One thing I still don&#039;t know about is whether Drupal&#039;s cron will be able to handle the site after a few months and a few hundred more feeds... but we&#039;ll see!&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://www.midwesternmac.com/blogs/geerlingguy/managing-news-revolutionary%E2%80%94not-evolutionary%E2%80%94step-drupal&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Fri, 23 Oct 2009 04:58:15 +0000</pubDate>
</item>
<item>
<title>Dries Buytaert: Eén using Drupal</title>
<link>http://buytaert.net/een-using-drupal</link>
<description>Eén (Dutch for &#039;one&#039;), a public TV station reaching millions of people in Belgium, redesigned its website using &lt;a href=&quot;http://drupal.org&quot;&gt;Drupal&lt;/a&gt;: see &lt;a href=&quot;http://een.be&quot;&gt;http://een.be&lt;/a&gt;.
&lt;div class=&quot;figure&quot;&gt;
&lt;img src=&quot;http://buytaert.net/sites/buytaert.net/files/cache/drupal-een-500x500.jpg&quot; alt=&quot;Een&quot; style=&quot;border: 1px solid #ccc; padding: 4px;&quot;/&gt;
&lt;/div&gt;</description>
<pubDate>Fri, 23 Oct 2009 01:30:55 +0000</pubDate>
</item>
<item>
<title>Open Em Space: Em Space&#039;s top Drupal 6 modules (that aren&#039;t always in the limelight)</title>
<link>http://open.emspace.com.au/article/em-spaces-top-drupal-6-modules-arent-always-limelight</link>
<description>&lt;div class=&quot;field field-type-filefield field-field-article-image&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;img class=&quot;imagefield imagefield-field_article_image&quot; width=&quot;200&quot; height=&quot;200&quot; alt=&quot;&quot; src=&quot;http://open.emspace.com.au/sites/open.emspace.com.au/files/article_images/Drupal-logo-C366BDF9CE-seeklogo.com_.gif?1256197849&quot; /&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;style type=&quot;text/css&quot;&gt;
h3 { text-decoration: underline; }
div.item-container { border-top: 1px dotted #CCC; padding-bottom: 10px; }
&lt;/style&gt;&lt;p&gt;
&lt;strong&gt;Every development house and their dogs seem to have a &#039;TOP 10 DRUPAL MODULES&lt;/strong&gt; - Absolute definitive version!!&#039; blog post somewhere at the minute, and they all tend to be fairly similar - &#039;Views, CCK, Image&#039; etc... &lt;/p&gt;
&lt;p&gt;We have decided to go a different route, and do our own summary of drupal modules (and combinations) that we use all the time, which you may not have used before.&lt;/p&gt;</description>
<pubDate>Fri, 23 Oct 2009 00:00:54 +0000</pubDate>
</item>
<item>
<title>NodeOne: The new Feeds module</title>
<link>http://nodeone.se/blogg/drupal/new-feeds-module</link>
<description>&lt;p&gt;How do you aggregate feeds into a &lt;a href=&quot;/drupal&quot;&gt;Drupal website&lt;/a&gt;? Or import data from other sources like a CSV document? The answer to this is of course &lt;a href=&quot;http://drupal.org/project/feedapi&quot;&gt;FeedAPI&lt;/a&gt;! Or is it?&lt;/p&gt;
&lt;p&gt;FeedAPI has for long been the mainstream solution for this kind of problems. And a really good one! But very recently the guys over at &lt;a href=&quot;http://developmentseed.org/&quot;&gt;Development Seed&lt;/a&gt; (the creators and maintainers of FeedAPI) released a new alpha version of the &lt;a href=&quot;http://drupal.org/project/feeds&quot;&gt;Feeds&lt;/a&gt; module. I haven&#039;t had time to play around with it too much yet. But it seems to be very promising. The dependency of &lt;a href=&quot;http://drupal.org/project/ctools&quot;&gt;CTools&lt;/a&gt; and its plugin framework makes Feeds a lot more extensible than FeedAPI was. The code base and its API is more thought out and seems to be better prepared for scalability.&lt;/p&gt;
&lt;p&gt;I can&#039;t wait to use Feeds out in the wild! When I do so, I&#039;ll come back with a more in depth review.&lt;/p&gt;
&lt;div class=&quot;watcher_node&quot;&gt;&lt;a href=&quot;/user/0/watcher/toggle/345?destination=drupalplanet%2Ffeed&quot; class=&quot;watcher_node_toggle_watching_link&quot; title=&quot;Watch posts to be notified when other users comment on them or the posts are changed&quot;&gt;Du bevakar inte detta inlägg, klicka här för att börja bevaka&lt;/a&gt;&lt;/div&gt;</description>
<pubDate>Thu, 22 Oct 2009 18:48:37 +0000</pubDate>
</item>
<item>
<title>Geoff Hankerson: Bring Sanity to Your Web Site (&amp; Your Life)</title>
<link>http://geoffhankerson.com/node/110</link>
<description>&lt;p&gt; Only 9 seats left. Sign up at &lt;a href=&quot;http://www.strategicit.org/sanity&quot;&gt;http://www.strategicit.org/sanity&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img style=&quot;display: block; padding-bottom: 12px;&quot; src=&quot;http://farm1.static.flickr.com/188/395226087_9002872142.jpg&quot; border=&quot;0&quot; alt=&quot;Sanily&quot; /&gt;&lt;/p&gt;
&lt;h6 style=&quot;font-size: 9px; padding-bottom: 12px;&quot;&gt;Photo by &lt;a href=&quot;http://www.flickr.com/photos/darkpatator/&quot;&gt;http://www.flickr.com/photos/darkpatator/&lt;/a&gt; / &lt;a href=&quot;http://creativecommons.org/licenses/by/2.0/&quot;&gt;CC BY 2.0&lt;/a&gt;&lt;/h6&gt;
&lt;p&gt;NO CHARGE :: Class Limited to First 15 Applicants&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://geoffhankerson.com/node/110&quot; target=&quot;_blank&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Thu, 22 Oct 2009 18:19:10 +0000</pubDate>
</item>
<item>
<title>Nick Vidal: HTTP and the Push Model</title>
<link>http://nick.iss.im/2009/10/22/http-and-the-push-model/</link>
<description>&lt;p&gt;The Real-time Web seems to be the buzz of the moment, and there has been quite a debate comparing HTTP, XMPP and related technologies (Comet, Web sockets). Generally HTTP is associated with the Pull model, while XMPP is associated with the Push model. But it&amp;#8217;s very well possible to design an architecture that follows the Push model using HTTP.&lt;/p&gt;
&lt;p&gt;Let&amp;#8217;s see an example to illustrated the point: Nick and Debbie are friends and they have subscribed to each other&amp;#8217;s feed to receive updates. Their feeds are hosted on different servers.&lt;/p&gt;
&lt;p&gt;In the Pull model, Nick has to poll Debbie&amp;#8217;s server every time to check for updates from Debbie, and vice-versa.&lt;/p&gt;
&lt;p&gt;In the Push model, the flow goes something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt; Debbie publishes a new entry on her server (Push);&lt;/li&gt;
&lt;li&gt; Debbie&amp;#8217;s server lets Nick&amp;#8217;s server know that Debbie has published a new entry (Push);&lt;/li&gt;
&lt;li&gt; Nick polls his own server to receive updates (Pull).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice that the second step is a Push implemented in HTTP. Nick&amp;#8217;s server didn&amp;#8217;t have to poll Debbie&amp;#8217;s server every time to check for updates.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://nick.iss.im/2009/10/22/http-and-the-push-model/&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Thu, 22 Oct 2009 16:50:31 +0000</pubDate>
</item>
<item>
<title>Palantir: Pacific Northwest Drupal Summit</title>
<link>http://www.palantir.net/blog/pacific-northwest-drupal-summit</link>
<description>&lt;p&gt;It&#039;s been a busy fall here in the Pacific Northwest. In the last two months the area has hosted no less than four Drupal events. &lt;/p&gt;
&lt;p&gt;Things kicked off in late September with &lt;a href=&quot;http://drupalcamp.northstudio.com/&quot;&gt;DrupalCamp Victoria&lt;/a&gt;. A couple weeks later was the Seattle Drupal Clinic, an event specifically focused at introducing new users to Drupal. Two weeks after that was &lt;a href=&quot;http://drupalpdx.org/camp09/&quot;&gt;DrupalCamp Portland&lt;/a&gt;, and finally last week a group of Drupal luminaries gathered in Vancouver for the &lt;a href=&quot;http://groups.drupal.org/node/24642&quot;&gt;Drupal Contrib Code Sprint&lt;/a&gt;, which resulted in &lt;a href=&quot;http://drupal4hu.com/node/223&quot;&gt;usable versions of Views and Coder for Drupal 7&lt;/a&gt;! &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://www.palantir.net/sites/default/files/pnw-summit-logo.png&quot; alt=&quot;Pacific Northwest Drupal Summit&quot; align=&quot;right&quot; /&gt;Phew, that&#039;s a lot of Drupal! The best part is it&#039;s not over yet, Seattle will close off the Drupal season with the &lt;a href=&quot;http://pnwdrupalsummit.org/&quot;&gt;Pacific Northwest Drupal Summit&lt;/a&gt; this coming weekend, October 24-25.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.palantir.net/blog/pacific-northwest-drupal-summit&quot; target=&quot;_blank&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Thu, 22 Oct 2009 16:44:15 +0000</pubDate>
</item>
<item>
<title>Stéphane Corlosquet: Produce and Consume Linked Data with Drupal!</title>
<link>http://openspring.net/blog/2009/10/22/produce-and-consume-linked-data-with-drupal</link>
<description>&lt;p&gt;&lt;a href=&quot;http://openspring.net/sites/openspring.net/files/corl-etal-2009iswc_logo.png&quot;&gt;&lt;img style=&quot;float:right&quot; src=&quot;http://openspring.net/sites/openspring.net/files/corl-etal-2009iswc_logo_thumb.png&quot; alt=&quot;Drupal in the Linked Data Cloud&quot; /&gt;&lt;/a&gt;&lt;em&gt;Produce and Consume Linked Data with Drupal!&lt;/em&gt; is the title of the paper I will be presenting next week at the &lt;a href=&quot;http://iswc2009.semanticweb.org/&quot;&gt;8th International Semantic Web Conference (ISWC 2009)&lt;/a&gt; in Washington, DC. I wrote it at the end of M.Sc. at &lt;a href=&quot;http://www.deri.ie/&quot;&gt;DERI&lt;/a&gt;, in partnership with the &lt;a href=&quot;http://hms.harvard.edu/&quot;&gt;Harvard Medical School&lt;/a&gt; and the &lt;a href=&quot;http://www.massgeneral.org/&quot;&gt;Massachusetts General Hospital&lt;/a&gt; which is where I am &lt;a href=&quot;http://openspring.net/blog/2009/09/19/one-way-ticket-to-boston&quot;&gt;now working&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It presents the approach for using Drupal (or any other CMS) as a Linked Data producer and consumer platform. Some part of this approach were used in the &lt;a href=&quot;http://drupal.org/node/493030&quot;&gt;RDF API&lt;/a&gt; that Dries committed a few days ago to Drupal core. I have attached &lt;a href=&quot;http://openspring.net/sites/openspring.net/files/corl-etal-2009iswc.pdf&quot;&gt;full paper&lt;/a&gt;, and here is the abstract:&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://openspring.net/blog/2009/10/22/produce-and-consume-linked-data-with-drupal&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Thu, 22 Oct 2009 13:03:26 +0000</pubDate>
</item>
<item>
<title>Dries Buytaert: Lucas Arts using Drupal</title>
<link>http://buytaert.net/lucas-arts-using-drupal</link>
<description>Lucas Arts, the video game company of George Lucas, launched a stunning &lt;a href=&quot;http://drupal.org&quot;&gt;Drupal&lt;/a&gt; site for its upcoming MMORPG: &lt;em&gt;Star Wars, The Old Republic&lt;/em&gt;. Check out the website at: &lt;a href=&quot;http://www.swtor.com&quot;&gt;http://www.swtor.com&lt;/a&gt;. &lt;em&gt;The Force is strong with Drupal!&lt;/em&gt;
&lt;div class=&quot;figure&quot;&gt;
&lt;img src=&quot;http://buytaert.net/sites/buytaert.net/files/cache/drupal-star-wars-game-500x500.jpg&quot; alt=&quot;Star wars game&quot; style=&quot;border: 1px solid #ccc; padding: 4px;&quot;/&gt;
&lt;/div&gt;
PS: in 2006, the &lt;a href=&quot;http://lullabot.com&quot;&gt;Lullabots&lt;/a&gt; and myself visited Skywalker Ranch, the private workplace of George Lucas, to get &lt;a href=&quot;http://buytaert.net/album/san-francisco-2006/light-saber-2&quot;&gt;some lightsaber training&lt;/a&gt;.</description>
<pubDate>Thu, 22 Oct 2009 11:40:31 +0000</pubDate>
</item>
<item>
<title>Janak Singh: Drupal Custom Pager navigation</title>
<link>http://janaksingh.com/blog/drupal-custom-pager-navigation-73</link>
<description>&lt;!-- google_ad_section_start --&gt;&lt;p&gt;For my portfolio site I wanted each image node (CCK + imagefield) to have a thumbnail strip of 10 or so images from the same category. Very simple stuff I thought. A quick search and I came across fantastic module called &lt;a href=&quot;http://drupal.org/project/custom_pagers&quot;&gt;Custom Pagers&lt;/a&gt; by &lt;a href=&quot;http://drupal.org/user/16496&quot;&gt;Eaton&lt;/a&gt;. This highly flexible module provides you a &lt;b&gt;Next&lt;/b&gt; and &lt;b&gt;Previous&lt;/b&gt; custom pager that you can display in your node pages. This was perfect for blog nodes but I wanted more control over the pager and I only wanted nodes to be pulled out from the same taxonomy term as the node being displayed.. fairly simple idea:&lt;/p&gt;
&lt;!-- google_ad_section_end --&gt;</description>
<pubDate>Thu, 22 Oct 2009 11:13:17 +0000</pubDate>
</item>
<item>
<title>Ryan Szrama: Ubercart 2.0 and the Ubercore Initiative</title>
<link>http://www.bywombats.com/blog/10-22-2009/ubercart-20-and-ubercore-initiative</link>
<description>&lt;p&gt;With high spirits and much excitement for the future, Lyle and I polished up and released &lt;a href=&quot;http://drupal.org/node/610966&quot;&gt;Ubercart 2.0&lt;/a&gt; today. Thanks to all those who took notice, and an even bigger thanks to the dozens of contributors who made the release a reality.&lt;/p&gt;
&lt;p&gt;Features of the release should come as no surprise, as most people have been using Ubercart 2.x for some time based on the project&#039;s &lt;a href=&quot;http://drupal.org/project/usage/ubercart&quot;&gt;usage statistics&lt;/a&gt; and personal experience. In the final days, we did iron out issues related to file downloads, role promotions, product kits, and Views integration. We also paved the way for smoother European use in conjunction with the &lt;a href=&quot;http://drupal.org/project/uc_vat&quot;&gt;UC2 VAT&lt;/a&gt; project.&lt;/p&gt;
&lt;p&gt;For those that are interested, continue reading for my reflections on the state of the Ubercart development process and code, including a community effort to realign both of these things on Drupal 7 with the &lt;a href=&quot;http://d7uc.org&quot;&gt;Drupal 7 Ubercore Initiative&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The teaser... Ubercart, D7, Small core influence -&gt; Ubercore (or, &lt;a href=&quot;http://d7uc.org&quot;&gt;d7uc&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.bywombats.com/blog/10-22-2009/ubercart-20-and-ubercore-initiative&quot; target=&quot;_blank&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Thu, 22 Oct 2009 04:38:38 +0000</pubDate>
</item>
<item>
<title>Lullabot: Drupal Voices 66: Jimmy Berry on the Drupal Test Framework</title>
<link>http://feedproxy.google.com/~r/lullabot-all/~3/Nvm2rEBEhN4/drupal-voices-66-jimmy-berry-drupal-test-framework</link>
<description>&lt;!--paging_filter--&gt;&lt;p&gt;&lt;a href=&quot;http://boombatower.com/&quot;&gt;Jimmy Berry&lt;/a&gt; (aka &lt;a href=&quot;http://drupal.org/user/214218&quot;&gt;boombatower&lt;/a&gt;) is the Drupal 7 Testing Subsystem Maintainer and maintainer of &lt;a href=&quot;http://testing.drupal.org&quot;&gt;testing.drupal.org.&lt;/a&gt; Testing has become an integral part of the core Drupal development process as Drupal 7 has adopted a test-driven development model, which &lt;a href=&quot;http://buytaert.net/drupal-7-testing-status-update-and-next-steps&quot;&gt;Dries explains here.&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;So Jimmy has picked up the testing torch for Drupal, and talks about his involvement with the &lt;a href=&quot;http://drupal.org/project/simpletest&quot;&gt;SimpleTest framework&lt;/a&gt; and helping getting it into Drupal core, how that&#039;s changed the core development process, and what it could mean if also applied to contributed modules as well.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.lullabot.com/audio/download/635/DrupalVoices066.mp3&quot;&gt;Download Audio&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Thu, 22 Oct 2009 03:46:07 +0000</pubDate>
</item>
<item>
<title>Dries Buytaert: Robbie Williams using Drupal</title>
<link>http://buytaert.net/robbie-williams-using-drupal</link>
<description>&lt;p&gt;A couple of weeks ago, Robbie Williams made &lt;a href=&quot;http://www.youtube.com/watch?v=FTJfNP1VEnw&quot;&gt;his comeback&lt;/a&gt; on
British television music talent show The X Factor, where he performed his new single &quot;Bodies&quot; for the first time live.&lt;/p&gt;
&lt;p&gt;With his comeback also comes a website refresh using &lt;a href=&quot;http://drupal.org&quot;&gt;Drupal&lt;/a&gt;: see &lt;a href=&quot;http://robbiewilliams.com&quot;&gt;http://robbiewilliams.com&lt;/a&gt;. The site was developed by an Acquia partner based in the UK.&lt;/p&gt;
&lt;div class=&quot;figure&quot;&gt;
&lt;img src=&quot;http://buytaert.net/sites/buytaert.net/files/cache/drupal-robbie-williams-500x500.jpg&quot; alt=&quot;Robbie williams&quot; style=&quot;border: 1px solid #ccc; padding: 4px;&quot;/&gt;
&lt;/div&gt;
&lt;object width=&quot;500&quot; height=&quot;392&quot;&gt;&lt;param name=&quot;movie&quot; value=&quot;http://www.youtube.com/v/Q5uKa1bDtsk&amp;hl=en&amp;fs=1&amp;&quot;&gt;&lt;/param&gt;&lt;param name=&quot;allowFullScreen&quot; value=&quot;true&quot;&gt;&lt;/param&gt;&lt;param name=&quot;allowscriptaccess&quot; value=&quot;always&quot;&gt;&lt;/param&gt;&lt;embed src=&quot;http://www.youtube.com/v/Q5uKa1bDtsk&amp;hl=en&amp;fs=1&amp;&quot; type=&quot;application/x-shockwave-flash&quot; allowscriptaccess=&quot;always&quot; allowfullscreen=&quot;true&quot; width=&quot;500&quot; height=&quot;392&quot;&gt;&lt;/embed&gt;&lt;/object&gt;</description>
<pubDate>Thu, 22 Oct 2009 02:01:49 +0000</pubDate>
</item>
<item>
<title>Growing Venture Solutions: Introducing Token Starterkit - Simple Introduction to Creating your own Drupal Tokens</title>
<link>http://growingventuresolutions.com/blog/introducing-token-starterkit-simple-introduction-creating-your-own-drupal-tokens</link>
<description>&lt;p&gt;There seems to be a new pattern emerging in Drupal and I want to let you know that the &lt;a href=&quot;http://drupal.org/project/token&quot;&gt;Token&lt;/a&gt; module has joined the bandwagon with a &quot;Token Starter Kit&quot;&lt;/p&gt;
&lt;h3&gt;History of the Starter Kit in Drupal: Zen Theming&lt;/h3&gt;
&lt;p&gt;When the Zen project started it&#039;s goal was to be a really solid base HTML theme with tons of comments in the templates so that a new themer could take it, modify it, and end up with a great theme. Unfortunately, that second step of modifying it meant that people ran into all sorts of support issues that were hard to debug and they were in trouble when a new version of Zen came out - they weren&#039;t really running Zen any more.&lt;/p&gt;
&lt;h3&gt;How to use the Token Starter Kit&lt;/h3&gt;
&lt;p&gt;The Token Starter Kit is meant to be similarly easy for folks to use. The idea is that if you just open up the token module itself and start adding tokens then you are &quot;hacking a contrib&quot; (modifying it) and you will have to remember to make those changes again when you upgrade. Bad news. It&#039;s also not particularly simple to understand how the module works (it&#039;s got includes, and hooks, oh my!).&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://growingventuresolutions.com/blog/introducing-token-starterkit-simple-introduction-creating-your-own-drupal-tokens&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 21 Oct 2009 23:16:12 +0000</pubDate>
</item>
<item>
<title>Alldrupalthemes: Does spam thrive during economic decline?</title>
<link>http://www.alldrupalthemes.com/drupal-blog/does-spam-thrive-during-economic-decline</link>
<description>&lt;p&gt;Are laid off IT workers discovering that sending spam is easier than getting a job these days? It sure seems that way, even with mollom running on all forms around a hundred spam comments get through every week, and they seem to get more clever every time.&lt;/p&gt;
&lt;p&gt;I just found the following comment below my review of the &lt;em&gt;Drupal 6 Javascript and jQuery&lt;/em&gt; book:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Submitted by san diego real estate (not verified) on Wed, 10/21/2009 - 18:55.&lt;/em&gt;&lt;br /&gt;
The only reason why I like this book is that this book developers deep into the usage of jQuery in themes and modules and there is interesting stuff in there for developers of any experience.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I can understand mollom didn&#039;t get that message because even I thought it was a real comment. I was much surprised to&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.alldrupalthemes.com/drupal-blog/does-spam-thrive-during-economic-decline&quot; target=&quot;_blank&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Wed, 21 Oct 2009 22:53:39 +0000</pubDate>
</item>
<item>
<title>Tag1 Consulting: Tag1 Now Hiring Interns</title>
<link>http://tag1consulting.com/blog/tag1-now-hiring-interns</link>
<description>&lt;p&gt;At the beginning of 2009, I was hired by Tag1 Consulting as Jeremy Andrews&#039; full time partner. A decidedly questionable decision on his part, but a great change for me! I used to work at the Open Source Lab at Oregon State University and I am currently the sysadmin for drupal.org. Working at the OSL and drupal.org spoiled me, I&#039;ll be completely honest about that. I got used to working with interesting new technologies and consistently pushing the limits of my knowledge.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://tag1consulting.com/blog/tag1-now-hiring-interns&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Wed, 21 Oct 2009 21:52:02 +0000</pubDate>
</item>
<item>
<title>Greg Holsclaw: Hook on Drush for Windows</title>
<link>http://www.tech-wanderings.com/drush-for-windows</link>
<description>&lt;p&gt;So I have heard of &lt;a href=&quot;http://drupal.org/project/drush&quot;&gt;Drush&lt;/a&gt; for years now, saw my first demo at the Boston DrupalCon but since I do all my dev work on a Windows machine I didn&#039;t catch the Drush wave (I kept hearing it was *nix only).&lt;/p&gt;
&lt;p&gt;It has always been in the back of my mind to keep looking back into Drush, but somehow I missed the major 2.0 update in June and that it works on Windows now. When I saw &lt;a href=&quot;http://morten.dk/blog/got-crush-drush&quot;&gt;Morton&#039;s Mac Drush post&lt;/a&gt; and revisited my Windows issue, and now I am a convert.&lt;/p&gt;
&lt;p&gt;Already there is an &lt;a href=&quot;http://drupal.org/node/594744&quot;&gt;install guide&lt;/a&gt; written two week ago that I have verified works perfectly for my Vista Business 64 bit machine. Drush is up and running on my dev system now and I am already addicted.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.tech-wanderings.com/drush-for-windows&quot; target=&quot;_blank&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Wed, 21 Oct 2009 20:00:34 +0000</pubDate>
</item>
<item>
<title>Geoff Hankerson: Installing Aegir Hosting System on OSX 10.6 with MAMP</title>
<link>http://geoffhankerson.com/node/109</link>
<description>&lt;p&gt;Cross posted at &lt;a href=&quot;http://groups.drupal.org/node/30270&quot; title=&quot;http://groups.drupal.org/node/30270&quot;&gt;http://groups.drupal.org/node/30270&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Aegir install on OSX Snow Leopard&lt;/p&gt;
&lt;p&gt;Aegir install instructions are fantastic if you run Debian/Ubuntu Linux. Some of the steps for OS X are a quite different&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://geoffhankerson.com/node/109&quot; target=&quot;_blank&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Wed, 21 Oct 2009 19:01:59 +0000</pubDate>
</item>
<item>
<title>Development Seed: Announcing Managing News: A Pluggable News + Data Aggregator</title>
<link>http://developmentseed.org/blog/2009/oct/21/announcing-managing-news-pluggable-news-data-aggregator</link>
<description>&lt;div class=&quot;field field-type-text field-field-subtitle&quot;&gt;
&lt;div class=&quot;field-items&quot;&gt;
&lt;div class=&quot;field-item odd&quot;&gt;
&lt;p&gt;From a daily news reader, to a platform for election monitoring in Afghanistan or swine flu preparedness in the United States&lt;/p&gt; &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#039;node-body&#039;&gt;&lt;p&gt;Managing News is a pluggable, open source news and data aggregator with visualization and workflow tools that&#039;s highly customizable and extensible. The code is now in open beta and is available for download on &lt;a href=&quot;http://www.managingnews.com/download&quot;&gt;www.managingnews.com/download&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2463/4031496689_0dd24a5705.jpg&quot; alt=&quot;http://managingnews.com&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://developmentseed.org/blog/2009/oct/21/announcing-managing-news-pluggable-news-data-aggregator&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Wed, 21 Oct 2009 17:14:57 +0000</pubDate>
</item>
<item>
<title>Good Old Drupal: Co-Maintainers Wanted!</title>
<link>http://goodold.se/blog/tech/co-maintainer-wanted</link>
<description>&lt;p&gt;The list of modules that I maintain has become quite long, and in the beginning of next year I&#039;ll have a little daughter (if the nurse guessed right on the gender). So the time that I have for being a good maintainer will be very limited.&lt;/p&gt;
&lt;p&gt;If you feel that you&#039;d like to help maintain any of the following modules, I would be very grateful!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://drupal.org/project/cobalt&quot;&gt;Cobalt&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://drupal.org/project/nodeformcols&quot;&gt;Node form columns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://drupal.org/project/oauth_common&quot;&gt;OAuth Common&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://drupal.org/project/services_oauth&quot;&gt;Services OAuth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://drupal.org/project/simple_geo&quot;&gt;Simple Geo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://drupal.org/project/jsonrpc_server&quot;&gt;JSONRPC Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://drupal.org/project/query_builder&quot;&gt;Query builder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Modules not on DO&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://github.com/hugowetterberg/services_oop&quot;&gt;Services OOP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://github.com/hugowetterberg/cssdry&quot;&gt;CSS DRY&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Experience of using &lt;strong&gt;git&lt;/strong&gt;, or the willingness to learn, is kind of a requirement, as all my development is done with git. The alternative is a patch-based workflow.&lt;/p&gt;</description>
<pubDate>Wed, 21 Oct 2009 06:36:30 +0000</pubDate>
</item>
<item>
<title>Lullabot: Drupal Voices 65: Konstantin Kafer on Optimizing Javascript and CSS</title>
<link>http://feedproxy.google.com/~r/lullabot-all/~3/EcYAQCa5xyE/drupal-voices-65-konstantin-kafer-optimizing-javascript-and-css</link>
<description>&lt;!--paging_filter--&gt;&lt;p&gt;&lt;a href=&quot;http://kkaefer.com/&quot;&gt;Konstantin Käfer&lt;/a&gt; (aka &lt;a href=&quot;http://drupal.org/user/14572&quot;&gt;kkafer&lt;/a&gt;) gives an overview of his Drupalcon Paris talk on optimizing the front-end Javascript and CSS -- including some &lt;a href=&quot;http://developer.yahoo.com/yslow/&quot;&gt;YSlow&lt;/a&gt; tips.&lt;/p&gt;
&lt;p&gt;Konstantin also talks a bit about his &lt;a href=&quot;http://drupal.org/project/sf_cache&quot;&gt;Support File Cache&lt;/a&gt; module that allows additional front-end optimizations by allowing you to control the bundling of CSS and Javascript files.&lt;/p&gt;
&lt;p&gt;He also talks a bit about the &lt;a href=&quot;http://frontenddrupal.com/&quot;&gt;Front-End Drupal&lt;/a&gt; that he co-wrote with &lt;a href=&quot;http://www.lullabot.com/drupal-voices/drupal-voices-60-emma-jane-hogbin-theming-and-bazaar-version-control&quot;&gt;Emma Jane Hogbin.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Finally, Konstantin talks a bit about some of his favorite changes in Drupal 7.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.lullabot.com/audio/download/633/DrupalVoices065.mp3&quot;&gt;Download Audio&lt;/a&gt;&lt;/p&gt;</description>
<pubDate>Wed, 21 Oct 2009 03:09:59 +0000</pubDate>
</item>
<item>
<title>Morten.dk - The King of Denmark: got a crush on drush</title>
<link>http://morten.dk/blog/got-crush-drush</link>
<description>&lt;div class=&quot;fieldgroup group-image&quot;&gt;
&lt;div class=&quot;content&quot;&gt;
&lt;div class=&quot;field-image-default&quot;&gt;
&lt;img src=&quot;http://morten.dk/sites/morten.dk/files/imagecache/20_20_crop/drushlove.jpg&quot; alt=&quot;&quot; title=&quot;Drupal shell geekyness&quot; class=&quot;imagecache imagecache-20_20_crop imagecache-default imagecache-20_20_crop_default&quot; width=&quot;20&quot; height=&quot;20&quot; /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;How I got drush to work on my macosx with mamp pro, and still my illustrator &amp;amp; photoshop works fine, how to do magick stuff with the terminal with out &lt;a href=&quot;/sites/morten.dk/files/images/argh.jpg&quot; class=&quot;fancybox&quot;&gt;deleting to much&lt;/a&gt;, and learning not to use my &lt;a href=&quot;http://www.wacom.com/bamboo/bamboo_fun.php&quot;&gt;pen&lt;/a&gt; to navigate my desktop...&lt;/p&gt;</description>
<pubDate>Wed, 21 Oct 2009 01:56:57 +0000</pubDate>
</item>
<item>
<title>Affinity Bridge: Drupal7 Contrib Module Upgrade Sprint</title>
<link>http://affinitybridge.com/blog/drupal7-contrib-module-upgrade-sprint</link>
<description>&lt;p&gt;This past weekend was the &lt;a href=&quot;http://groups.drupal.org/node/24642&quot;&gt;Drupal7 Contrib Module Upgrade Sprint&lt;/a&gt; that &lt;a href=&quot;http://drupal.org/user/9446&quot;&gt;K&amp;aacute;roly N&amp;eacute;gyesi&lt;/a&gt; (aka chx) organized at the &lt;a title=&quot;Now Public&quot; href=&quot;http://www.nowpublic.com/&quot;&gt;NowPublic&lt;/a&gt; offices in Vancouver. I spent a good part of Saturday there, helped out with coaching the one brave beginner who turned up to learn some of the tools for helping out in the community. Otherwise, after a bit of a rough start, the devs all hunkered down and made some Drupal magic, upgrading super important things like Views, Panels, database stuff, and various other bits and pieces of modules and themes.&lt;/p&gt;
&lt;p&gt;&lt;a title=&quot;D7 contrib sprint by arianek, on Flickr&quot; href=&quot;http://www.flickr.com/photos/arianek/4023847590/&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot;&gt;&lt;img src=&quot;http://farm3.static.flickr.com/2629/4023847590_98f25f162f.jpg&quot; alt=&quot;D7 contrib sprint&quot; width=&quot;500&quot; height=&quot;375&quot; /&gt;&lt;/p&gt;
&lt;p&gt; &amp;lt;!--break--&gt;
&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://affinitybridge.com/blog/drupal7-contrib-module-upgrade-sprint&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Tue, 20 Oct 2009 23:43:18 +0000</pubDate>
</item>
<item>
<title>Do it With Drupal: Speaker Spotlight: Earl Miles</title>
<link>http://feedproxy.google.com/~r/DoItWithDrupal/~3/Thij4pd2cBE/speaker-spotlight-earl-miles</link>
<description>&lt;p&gt;&lt;img src=&quot;http://www.doitwithdrupal.com/files/imagecache/120square/biopics/earl-headshot2.jpg&quot; alt=&quot;earl-headshot2&quot; title=&quot;earl-headshot2&quot; class=&quot;image-right&quot; height=&quot;120&quot; width=&quot;120&quot; /&gt;We are excited to announce that &lt;a href=&quot;http://www.angrydonuts.com/&quot;&gt;Earl Miles&lt;/a&gt; will be returning to &lt;a href=&quot;http://www.doitwithdrupal.com/&quot;&gt;Do It With Drupal&lt;/a&gt;! It is no exaggeration to say that Earl Miles single-handedly revolutionized the &lt;a href=&quot;http://drupal.org/&quot;&gt;Drupal&lt;/a&gt; community when he released the &lt;a href=&quot;http://drupal.org/project/views&quot;&gt;Views&lt;/a&gt; module late in 2005.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.doitwithdrupal.com/blog/speaker-spotlight-earl-miles&quot; target=&quot;_blank&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;http://feedproxy.google.com/~r/DoItWithDrupal/~3/Thij4pd2cBE/speaker-spotlight-earl-miles&quot;&gt;read more&lt;/a&gt;&lt;/p&gt;
</description>
<pubDate>Tue, 20 Oct 2009 22:06:32 +0000</pubDate>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:georss="http://www.georss.org/georss">
<updated>2010-09-07T21:45:39Z</updated>
<title>USGS M2.5+ Earthquakes</title>
<subtitle>Real-time, worldwide earthquake list for the past day</subtitle>
<link rel="self" href="http://earthquake.usgs.gov/earthquakes/catalogs/1day-M2.5.xml"/>
<link href="http://earthquake.usgs.gov/earthquakes/"/>
<author><name>U.S. Geological Survey</name></author>
<id>http://earthquake.usgs.gov/</id>
<icon>/favicon.ico</icon>
<entry><id>urn:earthquake-usgs-gov:ak:10076864</id><title>M 2.6, Central Alaska</title><updated>2010-09-07T21:08:45Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ak10076864.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/65_-150.jpg" alt="64.858&#176;N 150.864&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 21:08:45 UTC<br>Tuesday, September 7, 2010 01:08:45 PM at epicenter</p><p><strong>Depth</strong>: 11.20 km (6.96 mi)</p>]]></summary><georss:point>64.8581 -150.8643</georss:point><georss:elev>-11200</georss:elev><category label="Age" term="Past hour"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axbz</id><title>M 4.9, southern Qinghai, China</title><updated>2010-09-07T20:51:02Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axbz.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/35_95.jpg" alt="33.329&#176;N 96.332&#176;E" align="left" hspace="20" /><p>Tuesday, September 7, 2010 20:51:02 UTC<br>Wednesday, September 8, 2010 04:51:02 AM at epicenter</p><p><strong>Depth</strong>: 47.50 km (29.52 mi)</p>]]></summary><georss:point>33.3289 96.3324</georss:point><georss:elev>-47500</georss:elev><category label="Age" term="Past hour"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axbr</id><title>M 5.2, southern East Pacific Rise</title><updated>2010-09-07T19:54:29Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axbr.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/us2010axbr" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-55_-120.jpg" alt="53.198&#176;S 118.068&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 19:54:29 UTC<br>Tuesday, September 7, 2010 11:54:29 AM at epicenter</p><p><strong>Depth</strong>: 15.50 km (9.63 mi)</p>]]></summary><georss:point>-53.1979 -118.0676</georss:point><georss:elev>-15500</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axbp</id><title>M 5.0, South Island of New Zealand</title><updated>2010-09-07T19:49:57Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axbp.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-45_175.jpg" alt="43.437&#176;S 172.590&#176;E" align="left" hspace="20" /><p>Tuesday, September 7, 2010 19:49:57 UTC<br>Wednesday, September 8, 2010 07:49:57 AM at epicenter</p><p><strong>Depth</strong>: 1.00 km (0.62 mi)</p>]]></summary><georss:point>-43.4371 172.5902</georss:point><georss:elev>-1000</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ak:10076859</id><title>M 3.1, Andreanof Islands, Aleutian Islands, Alaska</title><updated>2010-09-07T19:20:05Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ak10076859.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/ak10076859" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/50_-175.jpg" alt="51.526&#176;N 175.798&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 19:20:05 UTC<br>Tuesday, September 7, 2010 10:20:05 AM at epicenter</p><p><strong>Depth</strong>: 22.20 km (13.79 mi)</p>]]></summary><georss:point>51.5259 -175.7979</georss:point><georss:elev>-22200</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ci:10793957</id><title>M 2.7, Southern California</title><updated>2010-09-07T18:50:42Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ci10793957.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/35_-115.jpg" alt="35.717&#176;N 116.960&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 18:50:42 UTC<br>Tuesday, September 7, 2010 11:50:42 AM at epicenter</p><p><strong>Depth</strong>: 7.80 km (4.85 mi)</p>]]></summary><georss:point>35.7170 -116.9597</georss:point><georss:elev>-7800</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ci:10793909</id><title>M 3.5, Southern California</title><updated>2010-09-07T17:29:13Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ci10793909.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/ci10793909" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/35_-115.jpg" alt="35.727&#176;N 116.957&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 17:29:13 UTC<br>Tuesday, September 7, 2010 10:29:13 AM at epicenter</p><p><strong>Depth</strong>: 4.50 km (2.80 mi)</p>]]></summary><georss:point>35.7273 -116.9567</georss:point><georss:elev>-4500</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ak:10076853</id><title>M 3.1, Andreanof Islands, Aleutian Islands, Alaska</title><updated>2010-09-07T17:08:19Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ak10076853.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/ak10076853" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/50_-175.jpg" alt="51.090&#176;N 176.131&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 17:08:19 UTC<br>Tuesday, September 7, 2010 08:08:19 AM at epicenter</p><p><strong>Depth</strong>: 16.50 km (10.25 mi)</p>]]></summary><georss:point>51.0899 -176.1314</georss:point><georss:elev>-16500</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axa9</id><title>M 6.3, Fiji region</title><updated>2010-09-07T16:13:32Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axa9.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-15_-180.jpg" alt="15.869&#176;S 179.261&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 16:13:32 UTC<br>Wednesday, September 8, 2010 04:13:32 AM at epicenter</p><p><strong>Depth</strong>: 10.00 km (6.21 mi)</p>]]></summary><georss:point>-15.8694 -179.2611</georss:point><georss:elev>-10000</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axa7</id><title>M 5.3, Kyrgyzstan</title><updated>2010-09-07T15:41:42Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axa7.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/us2010axa7" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/40_75.jpg" alt="39.476&#176;N 73.825&#176;E" align="left" hspace="20" /><p>Tuesday, September 7, 2010 15:41:42 UTC<br>Tuesday, September 7, 2010 09:41:42 PM at epicenter</p><p><strong>Depth</strong>: 39.70 km (24.67 mi)</p>]]></summary><georss:point>39.4759 73.8254</georss:point><georss:elev>-39700</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ci:10793837</id><title>M 2.7, Southern California</title><updated>2010-09-07T13:07:21Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ci10793837.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/35_-115.jpg" alt="35.725&#176;N 116.963&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 13:07:21 UTC<br>Tuesday, September 7, 2010 06:07:21 AM at epicenter</p><p><strong>Depth</strong>: 3.60 km (2.24 mi)</p>]]></summary><georss:point>35.7245 -116.9630</georss:point><georss:elev>-3600</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:nc:71451855</id><title>M 2.5, Northern California</title><updated>2010-09-07T13:06:56Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/nc71451855.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/nc71451855" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/40_-120.jpg" alt="39.210&#176;N 120.067&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 13:06:56 UTC<br>Tuesday, September 7, 2010 06:06:56 AM at epicenter</p><p><strong>Depth</strong>: 8.20 km (5.10 mi)</p>]]></summary><georss:point>39.2102 -120.0667</georss:point><georss:elev>-8200</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axax</id><title>M 5.4, Fiji region</title><updated>2010-09-07T12:49:01Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axax.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/us2010axax" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-15_-175.jpg" alt="14.361&#176;S 176.241&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 12:49:01 UTC<br>Wednesday, September 8, 2010 12:49:01 AM at epicenter</p><p><strong>Depth</strong>: 35.50 km (22.06 mi)</p>]]></summary><georss:point>-14.3605 -176.2406</georss:point><georss:elev>-35500</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axat</id><title>M 5.0, Kuril Islands</title><updated>2010-09-07T11:30:52Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axat.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/us2010axat" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/45_150.jpg" alt="45.858&#176;N 151.311&#176;E" align="left" hspace="20" /><p>Tuesday, September 7, 2010 11:30:52 UTC<br>Tuesday, September 7, 2010 11:30:52 PM at epicenter</p><p><strong>Depth</strong>: 30.30 km (18.83 mi)</p>]]></summary><georss:point>45.8582 151.3105</georss:point><georss:elev>-30300</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:mb:25757</id><title>M 2.7, western Montana</title><updated>2010-09-07T10:08:26Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/mb25757.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/45_-110.jpg" alt="44.951&#176;N 111.742&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 10:08:26 UTC<br>Tuesday, September 7, 2010 04:08:26 AM at epicenter</p><p><strong>Depth</strong>: 5.70 km (3.54 mi)</p>]]></summary><georss:point>44.9508 -111.7423</georss:point><georss:elev>-5700</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ak:10076821</id><title>M 2.7, Andreanof Islands, Aleutian Islands, Alaska</title><updated>2010-09-07T08:40:35Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ak10076821.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/ak10076821" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/50_-175.jpg" alt="51.201&#176;N 176.194&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 08:40:35 UTC<br>Monday, September 6, 2010 11:40:35 PM at epicenter</p><p><strong>Depth</strong>: 20.10 km (12.49 mi)</p>]]></summary><georss:point>51.2010 -176.1935</georss:point><georss:elev>-20100</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axas</id><title>M 4.9, southwest of Sumatra, Indonesia</title><updated>2010-09-07T07:22:13Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axas.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-5_105.jpg" alt="7.128&#176;S 103.263&#176;E" align="left" hspace="20" /><p>Tuesday, September 7, 2010 07:22:13 UTC<br>Tuesday, September 7, 2010 02:22:13 PM at epicenter</p><p><strong>Depth</strong>: 35.00 km (21.75 mi)</p>]]></summary><georss:point>-7.1275 103.2631</georss:point><georss:elev>-35000</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:nc:71451750</id><title>M 3.1, Central California</title><updated>2010-09-07T06:59:25Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/nc71451750.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/nc71451750" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/35_-120.jpg" alt="36.561&#176;N 121.068&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 06:59:25 UTC<br>Monday, September 6, 2010 11:59:25 PM at epicenter</p><p><strong>Depth</strong>: 8.40 km (5.22 mi)</p>]]></summary><georss:point>36.5605 -121.0677</georss:point><georss:elev>-8400</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ak:10076797</id><title>M 4.2, Kodiak Island region, Alaska</title><updated>2010-09-07T05:54:04Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ak10076797.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/ak10076797" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/55_-150.jpg" alt="56.980&#176;N 151.666&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 05:54:04 UTC<br>Monday, September 6, 2010 09:54:04 PM at epicenter</p><p><strong>Depth</strong>: 25.00 km (15.53 mi)</p>]]></summary><georss:point>56.9797 -151.6661</georss:point><georss:elev>-25000</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ak:10076786</id><title>M 3.4, Andreanof Islands, Aleutian Islands, Alaska</title><updated>2010-09-07T04:43:36Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ak10076786.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/ak10076786" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/50_-175.jpg" alt="51.098&#176;N 176.164&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 04:43:36 UTC<br>Monday, September 6, 2010 07:43:36 PM at epicenter</p><p><strong>Depth</strong>: 16.90 km (10.50 mi)</p>]]></summary><georss:point>51.0975 -176.1635</georss:point><georss:elev>-16900</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:ak:10076776</id><title>M 3.6, Andreanof Islands, Aleutian Islands, Alaska</title><updated>2010-09-07T03:43:43Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/ak10076776.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/50_-175.jpg" alt="51.471&#176;N 176.667&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 03:43:43 UTC<br>Monday, September 6, 2010 06:43:43 PM at epicenter</p><p><strong>Depth</strong>: 39.00 km (24.23 mi)</p>]]></summary><georss:point>51.4706 -176.6674</georss:point><georss:elev>-39000</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axaf</id><title>M 5.3, southern Iran</title><updated>2010-09-07T02:11:07Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axaf.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/us2010axaf" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/25_55.jpg" alt="27.147&#176;N 54.588&#176;E" align="left" hspace="20" /><p>Tuesday, September 7, 2010 02:11:07 UTC<br>Tuesday, September 7, 2010 05:41:07 AM at epicenter</p><p><strong>Depth</strong>: 27.90 km (17.34 mi)</p>]]></summary><georss:point>27.1465 54.5877</georss:point><georss:elev>-27900</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axad</id><title>M 4.9, Fiji region</title><updated>2010-09-07T01:42:39Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axad.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-20_-180.jpg" alt="19.657&#176;S 177.699&#176;W" align="left" hspace="20" /><p>Tuesday, September 7, 2010 01:42:39 UTC<br>Tuesday, September 7, 2010 01:42:39 PM at epicenter</p><p><strong>Depth</strong>: 390.40 km (242.58 mi)</p>]]></summary><georss:point>-19.6573 -177.6987</georss:point><georss:elev>-390400</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010axab</id><title>M 5.8, southwest of Sumatra, Indonesia</title><updated>2010-09-07T00:57:26Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010axab.php"/><link rel="related" type="application/cap+xml" href="http://earthquake.usgs.gov/earthquakes/catalogs/cap/us2010axab" /><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-5_105.jpg" alt="6.967&#176;S 103.657&#176;E" align="left" hspace="20" /><p>Tuesday, September 7, 2010 00:57:26 UTC<br>Tuesday, September 7, 2010 07:57:26 AM at epicenter</p><p><strong>Depth</strong>: 34.10 km (21.19 mi)</p>]]></summary><georss:point>-6.9665 103.6573</georss:point><georss:elev>-34100</georss:elev><category label="Age" term="Past day"/></entry>
<entry><id>urn:earthquake-usgs-gov:us:2010awby</id><title>M 5.2, North Island of New Zealand</title><updated>2010-09-06T22:48:33Z</updated><link rel="alternate" type="text/html" href="http://earthquake.usgs.gov/earthquakes/recenteqsww/Quakes/us2010awby.php"/><summary type="html"><![CDATA[<img src="http://earthquake.usgs.gov/images/globes/-40_175.jpg" alt="40.143&#176;S 176.657&#176;E" align="left" hspace="20" /><p>Monday, September 6, 2010 22:48:33 UTC<br>Tuesday, September 7, 2010 10:48:33 AM at epicenter</p><p><strong>Depth</strong>: 16.70 km (10.38 mi)</p>]]></summary><georss:point>-40.1430 176.6567</georss:point><georss:elev>-16700</georss:elev><category label="Age" term="Past day"/></entry>
</feed>

View File

@@ -0,0 +1,189 @@
<rss version="2.0">
<channel>
<generator>Rss generator</generator>
<pubDate>2009.10.20. 16:49:01</pubDate>
<title>Magyar Nemzet Online - Hírek</title>
<description>Magyar Nemzet Online - Hírek</description>
<link>http://www.mno.hu</link>
<language>HU</language>
<image>
<url>http://www.mno.hu/docfiles/rss/mno01.jpg</url>
<link>http://www.mno.hu</link>
<description>Magyar Nemzet Online - Hírek</description>
<title>Magyar Nemzet Online - Hírek</title>
<width>88</width>
<height>31</height>
</image>
<item>
<title>
Megint csak a balhé + Videó
</title>
<description>
Az egykori kiváló labdarúgó, Paul Gascoigne ezúttal sem futballtudása vagy esetleges edzői karrierjével került a címlapokra. A korábbi válogatott középpályás most egy snooker klubban okozott botrányt: lefejelte, majd bocsánatkérésként arcon csókolta az egyik kidobót. Utóbbi úriember meggondolatlanul cselekedett, mikor rászólt a sztárra, hogy tilos a dohányzás.
</description>
<pubDate>
2009-10-20 16:49 +0200
</pubDate>
<link>
http://www.mno.hu/portal/671000
</link>
<category>
Online
</category>
</item>
<item>
<title>
Az uniós tejalap csak filléreket hoz a magyar termelőknek
</title>
<description>
A magyar tejtermelők véleménye szerint az unió által kvóta alapon kioszthatónak minősített 280 millió eurós tejalap nem igazán hathatós segítség, mivel az tejkvóta-kilogrammonként mindössze 50 fillér többletet jelenthet egy termelő számára mondta az MTI megkeresésére kedden Bakos Erzsébet, a Tej Terméktanács szakmai koordinátora.<br /><a href="http://mno.hu/portal/670961"><strong><br /><span class="content_blog_lead" style="font-weight: bold"></span><strong><strong><strong><strong>•</strong></strong> Sürgősséggel döntenek a tejalapról</strong></strong></strong></a>
</description>
<pubDate>
2009-10-20 16:48 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670998
</link>
<category>
Online
</category>
</item>
<item>
<title>
Pluszban zárt a BUX
</title>
<description>
A Budapesti Értéktőzsde részvényindexe, a BUX 70,24 pontos, 0,33 százalékos emelkedéssel, 21.474,51 ponton zárt kedden.
</description>
<pubDate>
2009-10-20 16:44 +0200
</pubDate>
<link>
http://www.mno.hu/portal/671006
</link>
<category>
Online
</category>
</item>
<item>
<title>
Bajnait fogadja a pápa
</title>
<description>
Bajnai Gordon miniszterelnök november 13-án a Vatikánba utazik, hogy találkozzon XVI. Benedek pápával erősítette meg a nol.hu információját a kormányszóvivő.
</description>
<pubDate>
2009-10-20 16:40 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670997
</link>
<category>
Online
</category>
</item>
<item>
<title>
Élelmiszereink 11 százaléka „saját”
</title>
<description>
Egyre többet költünk élelmiszerre, és egyre kevesebb élelmiszert termelünk meg magunknak - derül ki a KSH 2000-2007 közötti éveket vizsgáló statisztikájából. Állati zsírt és cukrot kevesebbet fogyasztunk, gyorsétterembe viszont többet járunk.
</description>
<pubDate>
2009-10-20 16:31 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670989
</link>
<category>
Online
</category>
</item>
<item>
<title>
Leleplezte magát a Heves megyei szocialista közgyűlési elnök
</title>
<description>
Leleplezte magát Sós Tamás, a hevesi megyegyűlés elnöke, mert korábban azt állította, hogy az egri kórház fejlesztése csak magánműködtető bevonásával képzelhető el jelentette ki kedden Egerben a megyegyűlés Fidesz-frakciójának igazgatója. Ezzel szemben Sós Tamás most bejelentette, hogy az intézmény 4,6-4,8 milliárd forintnyi uniós fejlesztési forrásra számíthat mondta sajtótájékoztatón Herman István.
</description>
<pubDate>
2009-10-20 16:28 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670999
</link>
<category>
Online
</category>
</item>
<item>
<title>
Oszkóék gyomra nem korog
</title>
<description>
Állítólag százezer ember éhezik Magyarországon, vagyis ott, ahol „nagy a jólét”, ahová nem gyűrűznek be mindenféle válságok, és csupa remek ember rendelkezik az erőforrások és a döntési folyamatok felett. Végül is tízmillióhoz képest mi ez a százezer? Nyilván így gondolja ezt a teljes szocialista élcsapat is, mert egyetlen hangot sem hallattak az ügyben. De talán jobb is.
</description>
<pubDate>
2009-10-20 16:28 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670522
</link>
<category>
Online
</category>
</item>
<item>
<title>
Áramot vezetett a kerítésbe, hat év fegyházbüntetést kapott
</title>
<description>
Hat év fegyházbüntetésre ítélte kedden a Szabolcs-Szatmár-Bereg Megyei Bíróság azt a 41 éves kisari férfit, aki idén áprilisban áramot vezetett lakóházának kerítésébe, később pedig alkoholos állapotban késsel sebezte meg életveszélyesen az édesanyját, az ítélet nem jogerős.
</description>
<pubDate>
2009-10-20 16:22 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670994
</link>
<category>
Online
</category>
</item>
<item>
<title>
„A kormány elvesz, majd nagy csinnadrattával morzsákat oszt”
</title>
<description>
A tehetséggondozásra, az ország jövőjének a megalapozására kell, hogy legyen pénze a kormánynak mondta Bajnai Gordon a kedden bejelentett tehetségsegítő programról. Sió László, a Fidesz szakpolitikusa szerint a kormány két kézzel elvesz, majd nagy csinnadrattával morzsákat oszt. Igen alacsony a ténylegesen kifizetett források aránya reagált a hírre Cser-Palkovics András.
</description>
<pubDate>
2009-10-20 16:18 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670996
</link>
<category>
Online
</category>
</item>
<item>
<title>
Hőszivattyút telepítenek az iskolába
</title>
<description>
Mintegy 550 millió forintból teljesen felújítják a békési kistérségi általános iskola és pedagógiai szakszolgálat dr. Hepp Ferencről elnevezett tagintézményét közölte Békés polgármestere kedden sajtótájékoztatón. A hőszigetelési munkák mellett hőszivattyú telepítésével is próbálják csökkenteni az energiafogyasztást.
</description>
<pubDate>
2009-10-20 16:13 +0200
</pubDate>
<link>
http://www.mno.hu/portal/670987
</link>
<category>
Online
</category>
</item>
</channel>
</rss>

View File

@@ -0,0 +1,6 @@
Title,published,file,GUID
"Tubing is awesome",205200720,<?php print $files[0]; ?>,0
"Jeff vs Tom",428112720,<?php print $files[1]; ?>,1
"Attersee",1151766000,<?php print $files[2]; ?>,2
"H Street NE",1256326995,<?php print $files[3]; ?>,3
"La Fayette Park",1256326995,<?php print $files[4]; ?>,4

View File

@@ -0,0 +1,203 @@
<?php
print '<?xml version="1.0" encoding="utf-8" standalone="yes"?>';
?>
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:flickr="urn:flickr:" xmlns:media="http://search.yahoo.com/mrss/">
<title>Content from My picks</title>
<link rel="self" href="http://api.flickr.com/services/feeds/photoset.gne?set=72157603970496952&amp;nsid=28242329@N00&amp;lang=en-us" />
<link rel="alternate" type="text/html" href="http://www.flickr.com/photos/a-barth/sets/72157603970496952"/>
<id>tag:flickr.com,2005:http://www.flickr.com/photos/28242329@N00/sets/72157603970496952</id>
<icon>http://farm1.static.flickr.com/42/86410049_bd6dcdd5f9_s.jpg</icon>
<subtitle>Some of my shots I like best in random order.</subtitle>
<updated>2009-07-09T21:48:04Z</updated>
<generator uri="http://www.flickr.com/">Flickr</generator>
<entry>
<title>Tubing is awesome</title>
<link rel="alternate" type="text/html" href="http://www.flickr.com/photos/a-barth/3596408735/in/set-72157603970496952/"/>
<id>tag:flickr.com,2005:/photo/3596408735/in/set-72157603970496952</id>
<published>2009-07-09T21:48:04Z</published>
<updated>2009-07-09T21:48:04Z</updated>
<dc:date.Taken>2009-05-01T00:00:00-08:00</dc:date.Taken>
<content type="html">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/a-barth/&quot;&gt;Alex Barth&lt;/a&gt; posted a photo:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/a-barth/3596408735/&quot; title=&quot;Tubing is awesome&quot;&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3599/3596408735_ce2f0c4824_m.jpg&quot; width=&quot;240&quot; height=&quot;161&quot; alt=&quot;Tubing is awesome&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Virginia, 2009&lt;/p&gt;</content>
<author>
<name>Alex Barth</name>
<uri>http://www.flickr.com/people/a-barth/</uri>
</author>
<link rel="license" type="text/html" href="http://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="<?php print $image_urls[0]; ?>" />
<category term="color" scheme="http://www.flickr.com/photos/tags/" />
<category term="film" scheme="http://www.flickr.com/photos/tags/" />
<category term="virginia" scheme="http://www.flickr.com/photos/tags/" />
<category term="awesome" scheme="http://www.flickr.com/photos/tags/" />
<category term="ishootfilm" scheme="http://www.flickr.com/photos/tags/" />
<category term="va" scheme="http://www.flickr.com/photos/tags/" />
<category term="badge" scheme="http://www.flickr.com/photos/tags/" />
<category term="tubing" scheme="http://www.flickr.com/photos/tags/" />
<category term="fuji160c" scheme="http://www.flickr.com/photos/tags/" />
<category term="anfamiliebarth" scheme="http://www.flickr.com/photos/tags/" />
<category term="canon24l" scheme="http://www.flickr.com/photos/tags/" />
</entry>
<entry>
<title>Jeff vs Tom</title>
<link rel="alternate" type="text/html" href="http://www.flickr.com/photos/a-barth/2640019371/in/set-72157603970496952/"/>
<id>tag:flickr.com,2005:/photo/2640019371/in/set-72157603970496952</id>
<published>2009-07-09T21:45:50Z</published>
<updated>2009-07-09T21:45:50Z</updated>
<dc:date.Taken>2008-06-01T00:00:00-08:00</dc:date.Taken>
<content type="html">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/a-barth/&quot;&gt;Alex Barth&lt;/a&gt; posted a photo:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/a-barth/2640019371/&quot; title=&quot;Jeff vs Tom&quot;&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3261/2640019371_495c3f51a2_m.jpg&quot; width=&quot;240&quot; height=&quot;159&quot; alt=&quot;Jeff vs Tom&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
</content>
<author>
<name>Alex Barth</name>
<uri>http://www.flickr.com/people/a-barth/</uri>
</author>
<link rel="license" type="text/html" href="http://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="<?php print $image_urls[1]; ?>" />
<category term="b" scheme="http://www.flickr.com/photos/tags/" />
<category term="blackandwhite" scheme="http://www.flickr.com/photos/tags/" />
<category term="bw" scheme="http://www.flickr.com/photos/tags/" />
<category term="jeff" scheme="http://www.flickr.com/photos/tags/" />
<category term="tom" scheme="http://www.flickr.com/photos/tags/" />
<category term="washingtondc" scheme="http://www.flickr.com/photos/tags/" />
<category term="blackwhite" scheme="http://www.flickr.com/photos/tags/" />
<category term="dc" scheme="http://www.flickr.com/photos/tags/" />
<category term="nikon" scheme="http://www.flickr.com/photos/tags/" />
<category term="wideangle" scheme="http://www.flickr.com/photos/tags/" />
<category term="ilfordhp5" scheme="http://www.flickr.com/photos/tags/" />
<category term="foosball" scheme="http://www.flickr.com/photos/tags/" />
<category term="20mm" scheme="http://www.flickr.com/photos/tags/" />
<category term="nikonfe2" scheme="http://www.flickr.com/photos/tags/" />
<category term="800asa" scheme="http://www.flickr.com/photos/tags/" />
<category term="foosballtable" scheme="http://www.flickr.com/photos/tags/" />
<category term="wuzler" scheme="http://www.flickr.com/photos/tags/" />
<category term="wuzln" scheme="http://www.flickr.com/photos/tags/" />
<category term="tischfusball" scheme="http://www.flickr.com/photos/tags/" />
<category term="jeffmiccolis" scheme="http://www.flickr.com/photos/tags/" />
<category term="ilfordhp5800asa" scheme="http://www.flickr.com/photos/tags/" />
<category term="widean" scheme="http://www.flickr.com/photos/tags/" />
</entry>
<entry>
<title>Attersee 1</title>
<link rel="alternate" type="text/html" href="http://www.flickr.com/photos/a-barth/3686290986/in/set-72157603970496952/"/>
<id>tag:flickr.com,2005:/photo/3686290986/in/set-72157603970496952</id>
<published>2009-07-09T21:42:01Z</published>
<updated>2009-07-09T21:42:01Z</updated>
<dc:date.Taken>2009-06-01T00:00:00-08:00</dc:date.Taken>
<content type="html">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/a-barth/&quot;&gt;Alex Barth&lt;/a&gt; posted a photo:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/a-barth/3686290986/&quot; title=&quot;Attersee 1&quot;&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3606/3686290986_334c427e8c_m.jpg&quot; width=&quot;240&quot; height=&quot;238&quot; alt=&quot;Attersee 1&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Upper Austria, 2009&lt;/p&gt;</content>
<author>
<name>Alex Barth</name>
<uri>http://www.flickr.com/people/a-barth/</uri>
</author>
<link rel="license" type="text/html" href="http://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="<?php print $image_urls[2]; ?>" />
<category term="lake" scheme="http://www.flickr.com/photos/tags/" />
<category term="green" scheme="http://www.flickr.com/photos/tags/" />
<category term="water" scheme="http://www.flickr.com/photos/tags/" />
<category term="austria" scheme="http://www.flickr.com/photos/tags/" />
<category term="holga" scheme="http://www.flickr.com/photos/tags/" />
<category term="toycamera" scheme="http://www.flickr.com/photos/tags/" />
<category term="ishootfilm" scheme="http://www.flickr.com/photos/tags/" />
<category term="fujireala" scheme="http://www.flickr.com/photos/tags/" />
<category term="badge" scheme="http://www.flickr.com/photos/tags/" />
<category term="100asa" scheme="http://www.flickr.com/photos/tags/" />
<category term="attersee" scheme="http://www.flickr.com/photos/tags/" />
<category term="plasticlens" scheme="http://www.flickr.com/photos/tags/" />
<category term="colornegative" scheme="http://www.flickr.com/photos/tags/" />
</entry>
<entry>
<title>H Street North East</title>
<link rel="alternate" type="text/html" href="http://www.flickr.com/photos/a-barth/2640845934/in/set-72157603970496952/"/>
<id>tag:flickr.com,2005:/photo/2640845934/in/set-72157603970496952</id>
<published>2008-09-23T13:26:13Z</published>
<updated>2008-09-23T13:26:13Z</updated>
<dc:date.Taken>2008-06-01T00:00:00-08:00</dc:date.Taken>
<content type="html">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/a-barth/&quot;&gt;Alex Barth&lt;/a&gt; posted a photo:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/a-barth/2640845934/&quot; title=&quot;H Street North East&quot;&gt;&lt;img src=&quot;http://farm4.static.flickr.com/3083/2640845934_85c11e5a18_m.jpg&quot; width=&quot;240&quot; height=&quot;159&quot; alt=&quot;H Street North East&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Washington DC 2008&lt;br /&gt;
&lt;a href=&quot;http://dcist.com/2008/07/07/photo_of_the_day_july_7_2008.php&quot;&gt;Photo of the Day July 7 on DCist&lt;/a&gt;&lt;/p&gt;</content>
<author>
<name>Alex Barth</name>
<uri>http://www.flickr.com/people/a-barth/</uri>
</author>
<link rel="license" type="text/html" href="http://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="<?php print $image_urls[3]; ?>" />
<category term="nightphotography" scheme="http://www.flickr.com/photos/tags/" />
<category term="b" scheme="http://www.flickr.com/photos/tags/" />
<category term="blackandwhite" scheme="http://www.flickr.com/photos/tags/" />
<category term="bw" scheme="http://www.flickr.com/photos/tags/" />
<category term="night" scheme="http://www.flickr.com/photos/tags/" />
<category term="washingtondc" scheme="http://www.flickr.com/photos/tags/" />
<category term="blackwhite" scheme="http://www.flickr.com/photos/tags/" />
<category term="dc" scheme="http://www.flickr.com/photos/tags/" />
<category term="nikon" scheme="http://www.flickr.com/photos/tags/" />
<category term="dof" scheme="http://www.flickr.com/photos/tags/" />
<category term="wideangle" scheme="http://www.flickr.com/photos/tags/" />
<category term="explore" scheme="http://www.flickr.com/photos/tags/" />
<category term="ilfordhp5" scheme="http://www.flickr.com/photos/tags/" />
<category term="badge" scheme="http://www.flickr.com/photos/tags/" />
<category term="dcist" scheme="http://www.flickr.com/photos/tags/" />
<category term="20mm" scheme="http://www.flickr.com/photos/tags/" />
<category term="hstreet" scheme="http://www.flickr.com/photos/tags/" />
<category term="nikonfe2" scheme="http://www.flickr.com/photos/tags/" />
<category term="800asa" scheme="http://www.flickr.com/photos/tags/" />
<category term="hstreetne" scheme="http://www.flickr.com/photos/tags/" />
<category term="anfamiliebarth" scheme="http://www.flickr.com/photos/tags/" />
<category term="ilfordhp5800asa" scheme="http://www.flickr.com/photos/tags/" />
<category term="hstreetbynight" scheme="http://www.flickr.com/photos/tags/" />
<category term="forlaia" scheme="http://www.flickr.com/photos/tags/" />
</entry>
<entry>
<title>La Fayette Park</title>
<link rel="alternate" type="text/html" href="http://www.flickr.com/photos/a-barth/4209685951/in/set-72157603970496952/"/>
<id>tag:flickr.com,2005:/photo/4209685951/in/set-72157603970496952</id>
<published>2009-07-09T21:48:04Z</published>
<updated>2009-07-09T21:48:04Z</updated>
<dc:date.Taken>2009-05-01T00:00:00-08:00</dc:date.Taken>
<content type="html">&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/people/a-barth/&quot;&gt;Alex Barth&lt;/a&gt; posted a photo:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.flickr.com/photos/a-barth/3596408735/&quot; title=&quot;Tubing is fun&quot;&gt;&lt;img src=&quot;http://farm3.staticflickr.com/2675/4209685951_cb073de96f_m.jpg&quot; width=&quot;239&quot; height=&quot;240&quot; alt=&quot;La Fayette park&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Virginia, 2009&lt;/p&gt;</content>
<author>
<name>Alex Barth</name>
<uri>http://www.flickr.com/people/a-barth/</uri>
</author>
<link rel="license" type="text/html" href="http://creativecommons.org/licenses/by-nc/2.0/deed.en" />
<link rel="enclosure" type="image/jpeg" href="<?php print $image_urls[4]; ?>" />
<category term="color" scheme="http://www.flickr.com/photos/tags/" />
<category term="film" scheme="http://www.flickr.com/photos/tags/" />
<category term="virginia" scheme="http://www.flickr.com/photos/tags/" />
<category term="awesome" scheme="http://www.flickr.com/photos/tags/" />
<category term="ishootfilm" scheme="http://www.flickr.com/photos/tags/" />
<category term="va" scheme="http://www.flickr.com/photos/tags/" />
<category term="badge" scheme="http://www.flickr.com/photos/tags/" />
<category term="tubing" scheme="http://www.flickr.com/photos/tags/" />
<category term="fuji160c" scheme="http://www.flickr.com/photos/tags/" />
<category term="anfamiliebarth" scheme="http://www.flickr.com/photos/tags/" />
<category term="canon24l" scheme="http://www.flickr.com/photos/tags/" />
</entry>
</feed>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,248 @@
<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF xmlns="http://purl.org/rss/1.0/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:georss="http://www.georss.org/georss">
<channel rdf:about="http://www.magentosites.net/rss.xml">
<title xml:lang="en">Magento Sites Network - A directory listing of Magento Commerce stores</title>
<link>http://www.magentosites.net/</link>
<description xml:lang="en"></description>
<items rdf:nodeID="b1"/>
<sy:updatePeriod>daily</sy:updatePeriod>
</channel>
<rdf:Seq rdf:nodeID="b1">
<rdf:li>http://www.magentosites.net/node/3473</rdf:li>
<rdf:li>http://www.magentosites.net/node/3472</rdf:li>
<rdf:li>http://www.magentosites.net/node/3471</rdf:li>
<rdf:li>http://www.magentosites.net/node/3470</rdf:li>
<rdf:li>http://www.magentosites.net/node/3468</rdf:li>
<rdf:li>http://www.magentosites.net/node/3466</rdf:li>
<rdf:li>http://www.magentosites.net/node/3465</rdf:li>
<rdf:li>http://www.magentosites.net/node/3464</rdf:li>
<rdf:li>http://www.magentosites.net/node/3463</rdf:li>
<rdf:li>http://www.magentosites.net/node/3461</rdf:li>
<rdf:li>http://www.magentosites.net/node/3459</rdf:li>
<rdf:li>http://www.magentosites.net/node/3458</rdf:li>
<rdf:li>http://www.magentosites.net/node/3457</rdf:li>
<rdf:li>http://www.magentosites.net/node/3456</rdf:li>
<rdf:li>http://www.magentosites.net/node/3455</rdf:li>
<rdf:li>http://www.magentosites.net/node/3454</rdf:li>
<rdf:li>http://www.magentosites.net/node/3453</rdf:li>
<rdf:li>http://www.magentosites.net/node/3452</rdf:li>
<rdf:li>http://www.magentosites.net/node/3451</rdf:li>
<rdf:li>http://www.magentosites.net/node/3450</rdf:li>
</rdf:Seq>
<item rdf:about="http://www.magentosites.net/node/3473">
<title>Gezondheidswebwinkel</title>
<link>http://www.magentosites.net/store/2010/04/28/gezondheidswebwinkel/index.html</link>
<description>&lt;p&gt;Gezondheidswebwinkrl.nl is een online shop voor reformhuis gebaseerde producten.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-28T03:21:33Z</dc:date>
<dc:creator>dzinestudio</dc:creator>
<dc:subject>Health &amp; Personal Care</dc:subject>
<dc:subject>Grocery, Health &amp; Beauty</dc:subject>
<georss:point>52.214007 6.892291</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3472">
<title>MyBobino.com</title>
<link>http://www.magentosites.net/store/2010/04/26/mybobinocom/index.html</link>
<description>&lt;p&gt;The Bobino helps you organize and shorten your iPod or cell phone earbuds. No more messy lumps of cord in your pockets or purse. Go to www.mybobino.com.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-26T20:04:09Z</dc:date>
<dc:creator>TacovanhetReve</dc:creator>
<dc:subject>Computer Accessories</dc:subject>
<dc:subject>Gifts</dc:subject>
<dc:subject>MP3 &amp; Media Players</dc:subject>
<dc:subject>Computers &amp; Office</dc:subject>
<dc:subject>Electronics</dc:subject>
<dc:subject>Gifts, Foods &amp; Drinks</dc:subject>
<dc:subject>Other</dc:subject>
<georss:point>51.589322 4.774491</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3471">
<title>Boxershorts.nl</title>
<link>http://www.magentosites.net/store/2010/04/26/boxershortsnl/index.html</link>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-26T12:34:54Z</dc:date>
<dc:creator>tomashesseling</dc:creator>
<dc:subject>Gifts</dc:subject>
<dc:subject>Lingerie</dc:subject>
<dc:subject>Clothing, Shoes &amp; Jewelry</dc:subject>
<dc:subject>Gifts, Foods &amp; Drinks</dc:subject>
<dc:subject>Other</dc:subject>
<georss:point>52.097938 5.109197</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3470">
<title>Zapa</title>
<link>http://www.magentosites.net/store/2010/04/26/zapa/index.html</link>
<description>&lt;p&gt;The Zapa's webshop is based on Magento community edition.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-26T08:21:34Z</dc:date>
<dc:creator>wecom</dc:creator>
<dc:subject>Clothing &amp; Accessories</dc:subject>
<dc:subject>Clothing, Shoes &amp; Jewelry</dc:subject>
<georss:point>48.872290 2.363026</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3468">
<title>Bandenshoppen</title>
<link>http://www.magentosites.net/store/2010/04/26/bandenshoppen/index.html</link>
<description>&lt;p&gt;Bandenshoppen is een Magento webshop waarin voor iedere auto de juiste band te vinden is. Of het nu is voor een bestelwagen, SUV of personenwagen, de juiste band van alle grote merken kunt u hier eenvoudig vinden.&lt;/p&gt;
&lt;p&gt;Met slechts de code op uw band kiest u de juiste winter- of zomerband.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-26T07:41:01Z</dc:date>
<dc:creator>Eigen en Wijze Communicatie</dc:creator>
<dc:subject>Automotive</dc:subject>
<dc:subject>Tools, Auto &amp; Industrial</dc:subject>
<georss:point>52.792620 4.786409</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3466">
<title>Furniture For You (Rugby) LTD</title>
<link>http://www.magentosites.net/store/2010/04/23/furniture-for-you-rugby-ltd/index.html</link>
<description>&lt;p&gt;Furniture For You (Rugby) LTD is a large discount furniture store based in Rugby, Warwickshire. Huge selections of Bedroom, Living room &amp;amp; Dining room furniture made by top manufacturers in the UK, Italy &amp;amp; Germany.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-23T13:34:28Z</dc:date>
<dc:creator>furnitureforyoultd</dc:creator>
<dc:subject>Furniture &amp; Décor</dc:subject>
<dc:subject>Home &amp; Garden</dc:subject>
<georss:point>52.372879 -1.286754</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3465">
<title>GoedHip</title>
<link>http://www.magentosites.net/store/2010/04/22/goedhip/index.html</link>
<description>&lt;p&gt;GoedHip is een Magento webshop met hippe fair trade, ecologische en biologische producten.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-22T12:02:35Z</dc:date>
<dc:creator>Goedhip</dc:creator>
<dc:subject>Gifts</dc:subject>
<dc:subject>Gifts, Foods &amp; Drinks</dc:subject>
<georss:point>52.091262 5.122748</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3464">
<title>Lampenwereld</title>
<link>http://www.magentosites.net/store/2010/04/22/lampenwereld/index.html</link>
<description>&lt;p&gt;Online Lightstore with a big variation of lights. All prizes include tax, lightbulb and shipment.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-22T11:43:40Z</dc:date>
<dc:creator>lampenwereld</dc:creator>
<dc:subject>Furniture &amp; Décor</dc:subject>
<dc:subject>Home &amp; Garden</dc:subject>
<georss:point>51.581985 4.732600</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3463">
<title>Bumblejax</title>
<link>http://www.magentosites.net/store/2010/04/21/bumblejax/index.html-0</link>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-21T21:42:01Z</dc:date>
<dc:creator>coreyd74</dc:creator>
<dc:subject>Art, Culture &amp; Leisure</dc:subject>
<dc:subject>Photography</dc:subject>
<georss:point>47.622748 -122.334355</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3461">
<title>SendraValencia</title>
<link>http://www.magentosites.net/store/2010/04/20/sendravalencia/index.html</link>
<description>&lt;p&gt;Tienda de Botas Sendra en Internet. Sendra Boots Store.&lt;br /&gt;
En Botas Sendra Valencia se ha cuidado al máximo el diseño y la navegación. Integración con almacenes y Courier.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-20T09:32:36Z</dc:date>
<dc:creator>Onestic</dc:creator>
<dc:subject>Shoes</dc:subject>
<dc:subject>Clothing, Shoes &amp; Jewelry</dc:subject>
<georss:point>39.469008 -0.371995</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3459">
<title>Snowcountry</title>
<link>http://www.magentosites.net/store/2010/04/19/snowcountry/index.html</link>
<description>&lt;p&gt;Freeski en Snowboard webshop&lt;/p&gt;
&lt;p&gt;Snowcountry.nl richt zich voornamelijk op de actieve boarder en skiër, je vind hier merken die niet op elke straathoek te koop zijn, sterker nog veelal zijn de ski- en snowboard collecties alleen bij Snowcountry online verkrijgbaar.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-19T15:35:29Z</dc:date>
<dc:creator>Snowcountry</dc:creator>
<dc:subject>All Sports &amp; Outdoors</dc:subject>
<dc:subject>Athletic &amp; Outdoor Clothing</dc:subject>
<dc:subject>Outdoor Recreation</dc:subject>
<dc:subject>Sports &amp; Outdoors</dc:subject>
<georss:point>52.179226 5.507952</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3458">
<title>Floriculture Australia - Wholesale Florist</title>
<link>http://www.magentosites.net/store/2010/04/18/floriculture-australia-wholesale-florist/index.html</link>
<description>&lt;p&gt;Floriculture Australia Wholesale Florist was set up as a cut flower distribution business a few years ago with a distinct difference to other wholesale florists - we quality check all product, we provide a high level of personal and professional service and a respect for the environment, ourselves and those we deal with.&lt;/p&gt;
&lt;p&gt;Originally, the family bought a country property to have some room for the dogs to roam around back in the mid 1990s. This property just happened to have some floristry foliage trees, lavender and daffodils. Max, then in his late 20s, took on a new industry, a new career and loads and loads of hard work.&lt;/p&gt;
&lt;p&gt;Today, the business has expanded, Max has married had some kiddies, further property purchases have been made, and a workforce employed to help with all that hard work!&lt;/p&gt;
&lt;p&gt;The driving philosophy has always been to produce and distribute quality flowers in a sustainable manner. We use natural forms of pest control, so you can be sure any flowers supplied by us at Floriculture.com.au are not only fresh and beautiful they are safe to handle.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-18T12:52:09Z</dc:date>
<dc:creator>MaxTheFlowerGuy</dc:creator>
<dc:subject>Flowers</dc:subject>
<dc:subject>Gifts, Foods &amp; Drinks</dc:subject>
<georss:point>-37.814470 145.437914</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3457">
<title>Flower Bunch</title>
<link>http://www.magentosites.net/store/2010/04/18/flower-bunch/index.html</link>
<description>&lt;p&gt;FlowerBunch has Australia wide flower delivery direct from our own flower farm and wholesale flower distribution shed. This ensures the freshest flowers, discounted flowers and seasonal specials and next day free flower delivery.&lt;/p&gt;
&lt;p&gt;The driving philosophy for the company has always been to produce quality flowers in a sustainable manner. Our flowers are grown using hydroponic organic flower production practices, so you can be sure any flowers supplied by us at http://FlowerBunch.com.au are not only fresh and beautiful, they are also safe to handle.&lt;/p&gt;
&lt;p&gt;Our farm fresh cut flower delivery is available to Sydney, Melbourne, Brisbane, Canberra, Adelaide and all eastern states of Australia. Some remote areas may take a day or two extra for delivery. All flowers are sent direct from our farm, not through a relay/referral service.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-18T12:42:54Z</dc:date>
<dc:creator>MaxTheFlowerGuy</dc:creator>
<dc:subject>Flowers</dc:subject>
<dc:subject>Gifts, Foods &amp; Drinks</dc:subject>
<georss:point>-37.812267 145.432457</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3456">
<title>Conec - elektronische Bauelemente</title>
<link>http://www.magentosites.net/store/2010/04/15/conec-elektronische-bauelemente/index.html</link>
<description>&lt;p&gt;CONEC entwickelt und produziert in modernen Produktionsstätten mit Hightech-Technologie in zuverlässigen Produktionsprozessen. Die Vielfalt der CONEC-Produktpalette bietet zuverlässige und effektive Antworten auf alle Fragen der Verbindungstechnik, bei Geräteherstellern, Subunternehmern und Kabelverarbeitern. CONEC ist in allen Industriebereichen zu Hause.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-15T09:07:48Z</dc:date>
<dc:creator>simone.meisner</dc:creator>
<dc:subject>Other</dc:subject>
<georss:point>51.674817 8.377200</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3455">
<title>Liemke - technisch chemische Artikel</title>
<link>http://www.magentosites.net/store/2010/04/15/liemke-technisch-chemische-artikel/index.html</link>
<description>&lt;p&gt;Hochwertige Klebstoffe, Aerosole &amp;amp; Reiniger aus dem Hause LIEMKE&lt;/p&gt;
&lt;p&gt;Überzeugen Sie sich von der Qualität der professionellen chemischen Produkte. Das Sortiment der Hochleistungs-Produkte der Qualitätsmarke &quot;LK&quot; umfasst hochwertige Klebstoffe, Schmiermittel, Aerosole, Dichtstoffe, Silikone, Reiniger, Desinfektionsmittel und Trennmittel für chemisch-technische Anwendungen, für die Bauchemie und die professionelle Desinfektion.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-15T08:59:45Z</dc:date>
<dc:creator>simone.meisner</dc:creator>
<dc:subject>Other</dc:subject>
<georss:point>51.956925 8.578393</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3454">
<title>Peitz Werkzeug und Outdoorbekleidung</title>
<link>http://www.magentosites.net/store/2010/04/15/peitz-werkzeug-und-outdoorbekleidung/index.html</link>
<description>&lt;p&gt;Bei Peitz Werkzeuge finden Sie ein breites Sortiment an erstklassigen Werkzeugen und hochwertigem Outdoor-Equipment. Wir führen die Marken Hitachi, Milwaukee, Matador, BP, Yeti, Nordisk, Tortuga u.v.m.&lt;/p&gt;
&lt;p&gt;Über 1.000 Artikel stehen in diesem Shop zur Verfügung. &lt;/p&gt;
&lt;p&gt;Erfahrung, Service und günstige Preise überzeugen!&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-15T08:46:18Z</dc:date>
<dc:creator>simone.meisner</dc:creator>
<dc:subject>Power &amp; Hand Tools</dc:subject>
<dc:subject>Tools, Auto &amp; Industrial</dc:subject>
<georss:point>51.791418 8.420595</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3453">
<title>Großewinkelmann Tor- und Zaunsysteme</title>
<link>http://www.magentosites.net/store/2010/04/15/grossewinkelmann-tor-und-zaunsysteme/index.html</link>
<description>&lt;p&gt;Großewinkelmann bietet Zaun- und Torsysteme sowie Stall- und Weidetechnik an. Produktideen und jahrzentelange Erfahrung bringen den entscheidenden Vorteil für den Kunden. Seit mehr als 60 Jahren steht Großewinkelmann für Qualität und persönlichen Service rund um Zäune und Tore.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-15T08:39:25Z</dc:date>
<dc:creator>simone.meisner</dc:creator>
<dc:subject>Other</dc:subject>
<georss:point>51.862318 8.435454</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3452">
<title>Möbius Werbemittel</title>
<link>http://www.magentosites.net/store/2010/04/15/mobius-werbemittel/index.html</link>
<description>&lt;p&gt;Möbius Werbemittel bietet einen Onlineshop für Werbemittel und Werbegeschenke. Ein breites Sortiment aus vielen Bereichen bietet optimale Auswahl. Finden Sie Feuerzeuge, Schluesselanhänger, Schirme, Tassen, Schreibgeraete, Buero- und Geschäftsausstattung und vieles mehr.&lt;br /&gt;
Der schnelle Druckservice und die kompetente Beratung runden das Angebot ab.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-15T08:05:42Z</dc:date>
<dc:creator>simone.meisner</dc:creator>
<dc:subject>Other</dc:subject>
<georss:point>49.544584 8.346575</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3451">
<title>watch store</title>
<link>http://www.magentosites.net/store/2010/04/14/watch-store/index.html</link>
<description>&lt;p&gt;watches, is a fast growing e-shop in Greece. You can find a big variety of whell priced watches.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-14T21:44:32Z</dc:date>
<dc:creator>webo2</dc:creator>
<dc:subject>Watches</dc:subject>
<dc:subject>Clothing, Shoes &amp; Jewelry</dc:subject>
<georss:point>37.443287 24.942652</georss:point>
</item>
<item rdf:about="http://www.magentosites.net/node/3450">
<title>Fishingtime.com</title>
<link>http://www.magentosites.net/store/2010/04/14/fishingtimecom/index.html</link>
<description>&lt;p&gt;Fishingtime.com is a very fast growing fishing tackle e-shop. You can find everything about fishing in competitive prices and services.&lt;/p&gt;</description>
<dc:date rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2010-04-14T07:21:29Z</dc:date>
<dc:creator>dimmeneidis</dc:creator>
<dc:subject>All Sports &amp; Outdoors</dc:subject>
<dc:subject>Sports &amp; Outdoors</dc:subject>
<georss:point>37.953416 23.692998</georss:point>
</item>
</rdf:RDF>

View File

@@ -0,0 +1,87 @@
Title,Body,published,GUID
"Ut wisi enim ad minim veniam", "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.",205200720,2
"Duis autem vel eum iriure dolor", "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.",428112720,3
"Nam liber tempor", "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.",1151766000,1
Typi non habent"", "Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem.",1256326995,4
"Lorem ipsum","Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",1251936720,1
"Investigationes demonstraverunt", "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",946702800,5
"Claritas est etiam", "Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum.",438112720,6
"Mirum est notare", "Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima.",1151066000,7
"Eodem modo typi", "Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.",1212891020,8
"Ut quam dolor", "Ut quam dolor, aliquam pretium elementum ac, auctor eu tortor.",1200837000,9
"Sed id dignissim lorem", "Donec nec urna mauris. Duis tincidunt",1201000000,10
"Aliquam feugiat diam", "Aliquam feugiat diam ac enim egestas ut cursus lacus fermentum.",1210922021,11
"Proin et fringilla leo", "Maecenas rhoncus velit quis nibh convallis pharetra.",1201832100,12
"Suspendisse potenti", "Integer commodo elit et arcu dapibus at hendrerit nunc dapibus.",1122838020,12
"Nunc eu lectus nisi", "Nunc eu lectus nisi, sed vestibulum mauris. Sed tincidunt vehicula sem, eu tincidunt massa ultrices vel.",1200827015,13
"Mauris tellus erat", "",1201845210,13
"Pellentesque porttitor gravida magna", "Pellentesque porttitor gravida magna, ut lacinia risus suscipit a. Nunc molestie molestie massa non auctor.",1211835320,14
"Pellentesque facilisis ultrices", "Pellentesque facilisis ultrices risus non porttitor. Donec dapibus velit in metus consectetur et ullamcorper velit volutpat.",1190835120,15
"Sed eros lectus", "Sed eros lectus, mollis vel commodo et, molestie vel urna.",1151825020,16
"Morbi consectetur fringilla dolor", "Morbi consectetur fringilla dolor. Morbi eleifend pharetra purus, non facilisis tortor ullamcorper sed.",1201745220,17
"Vivamus vitae lectus", "Vivamus vitae lectus ac urna tempus dapibus eu consectetur massa. Donec vitae arcu lectus, non ornare nunc. ",1201825020,18
"Fusce est felis", "Fusce est felis, tincidunt eu congue id, placerat nec massa.",1201836001,19
"Duis tristique velit", "Duis tristique velit vitae lacus malesuada sed commodo quam commodo.",1201832024,20
"In ac felis neque", "Integer dictum sapien eget nunc commodo convallis.",1201830020,21
"Proin a mi nulla", "Proin a mi nulla, sodales mattis nunc.",1201822020,21
"Pellentesque vitae massa", "Pellentesque vitae massa elementum augue varius suscipit at quis turpis.",1201810020,22
"Proin dapibus", "Nam pulvinar urna vel eros aliquam hendrerit.",1201636020,23
"Donec", "Nulla pulvinar felis nec nulla pretium id pulvinar risus scelerisque.",1201336020,24
"Quisque dictum sagittis purus", "Quisque dictum sagittis purus, nec tristique magna sagittis nec. Mauris tortor nisl, cursus sit amet varius vitae, posuere in ipsum.",1201236020,25
"Praesent", "Praesent tincidunt vulputate turpis non faucibus",1201132020,26
"Vestibulum", "Vestibulum volutpat interdum elit, quis aliquam leo lobortis non.",1201036020,27
"Aliquam", "Aliquam fringilla lobortis mollis.",1201022020,28
"Etiam faucibus", "Etiam faucibus, quam vitae sollicitudin sagittis, nisl metus ultricies risus, sed convallis nibh risus ut libero.",1201021020,29
"In hac habitasse", "Proin justo sapien, dapibus vel dictum non, vestibulum in neque.",1201010020,30
"Mauris risus dolor", "Maecenas rutrum diam suscipit mi feugiat venenatis.",1201005011,31
"Pellentesque habitant", "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.",1201001010,32
"Duis convallis", "Duis convallis quam non eros suscipit iaculis. Aliquam erat volutpat.",1201000010,33
"Proin adipiscing mi vel", "Proin nibh est, gravida ac condimentum viverra, hendrerit sodales tellus.",1200901010,34
"Nullam sodales", "Nullam sodales laoreet suscipit.",1200851010,35
"Duis ornare pulvinar purus", "Duis ornare pulvinar purus, at pulvinar nulla hendrerit vel.",1200801010,35
"Mauris sit amet nibh risus", "Mauris sit amet nibh risus, eget eleifend ante. Donec imperdiet ante quis ligula condimentum consectetur.",1200761010,36
"Fusce sit amet mi vitae", "Fusce sit amet mi vitae ipsum tempor rhoncus. Aenean sit amet diam erat, fringilla porttitor felis.",1200751010,37
"Cras blandit ornare", "Cras blandit ornare augue in tempor. Curabitur vitae dolor nibh, sed molestie metus.",1200601010,37
"Proin at eros", "Proin at eros ut est laoreet tristique. Duis ullamcorper, nisi eu gravida mattis, nibh ligula laoreet lectus, vitae elementum augue sem non nisl.",1200451010,38
"Vivamus sem purus", "Vivamus sem purus, viverra eget faucibus et, imperdiet vel massa.",1200551010,40
"Ut id nisl", "Morbi suscipit tincidunt ultrices. Suspendisse ornare aliquet elit ac accumsan.",1200121010,41
"Sed libero leo", "Phasellus ut sem sit amet justo sagittis placerat.",1200202010,42
"Quisque eros nunc", "Quisque eros nunc, iaculis id bibendum ut, imperdiet ut sem. Donec mi quam, ultricies sit amet ornare et, lacinia non libero.",1201222010,43
"Vestibulum vulputate", "Vestibulum vulputate, lacus quis fringilla imperdiet, nisi mi consequat lacus, eu sodales nisl nisi venenatis lacus.",1200022010,44
"Suspendisse sed", "Suspendisse sed est eget quam imperdiet laoreet.",1202200010,45
"Duis a lacus odio", "Duis a lacus odio. Nam luctus sapien id leo vestibulum tristique. Cras eu nisi velit, a aliquet turpis. Maecenas ligula est, ullamcorper vel placerat sit amet, scelerisque et lectus. Nunc velit massa; scelerisque et pretium non, sollicitudin vel tellus? Donec leo turpis, tempus dignissim placerat vel, venenatis a augue. Etiam condimentum porttitor urna, quis scelerisque justo pulvinar eget. Fusce nec nibh non enim mattis posuere. Cras pulvinar erat eget ante gravida congue. Pellentesque ultrices metus ut nunc suscipit id imperdiet nunc ornare. Nam consectetur erat quis massa congue vehicula eget vestibulum turpis. Morbi ac eleifend felis.",1202200010,45
"Vivamus tempor", "Vivamus tempor, mi a pulvinar convallis, massa enim pulvinar metus, nec fringilla quam ante eu erat. Cras dignissim, felis non euismod iaculis, est massa posuere diam, et auctor libero ipsum ac ipsum? Nullam iaculis, eros ac sodales sollicitudin; tellus ligula volutpat urna, eget mattis felis metus ac augue.",1202200010,46
"Sed molestie", "Sed molestie dolor sit amet neque dictum egestas? Aliquam vitae dui mi. Phasellus fermentum volutpat augue, quis ornare tortor facilisis ac. Suspendisse potenti. Duis facilisis massa at elit pharetra sit amet condimentum est pharetra.",1202200010,47
"Donec ut", "Donec ut est ipsum. Pellentesque porttitor eleifend neque non malesuada. Morbi sollicitudin varius dapibus. Morbi sit amet risus leo, eu suscipit urna. Suspendisse velit ligula, suscipit ac rhoncus eu, convallis in eros",1202100010,48
"Duis ut dolor sem", "Duis ut dolor sem. Donec convallis, nunc quis pulvinar tempus, leo est blandit lacus, et elementum lectus nisl et purus. Mauris eros eros, iaculis volutpat pretium euismod, porta id arcu. Integer tellus lacus, imperdiet sed vulputate varius, dignissim vel nisi. In porta molestie fermentum.",1202222101,49
"Fusce sodales luctus porta", "Fusce sodales luctus porta. Curabitur pellentesque tincidunt tristique. In hac habitasse platea dictumst.",1202123211,50
"In egestas lectus a sapien", "In egestas lectus a sapien sollicitudin nec blandit metus scelerisque. Proin et tortor eget risus congue sollicitudin. In auctor interdum turpis porta commodo. Donec faucibus elementum nibh, a egestas nisi tincidunt id.",1202232323,51
"Aliquam semper", "Aliquam semper egestas aliquet. Aliquam tristique velit sit amet leo sodales aliquam. Praesent cursus ipsum quis odio aliquet eu eleifend velit aliquam? Maecenas consequat lobortis augue, at venenatis enim hendrerit quis.",1202242452,52
"Curabitur tortor", "Curabitur tortor turpis, commodo eu pretium ac, sollicitudin a augue. ",1201341344,53
"Duis venenatis", "Duis venenatis lorem vel sapien suscipit consectetur. In vel lectus neque, ut rutrum sapien.",1204564533,54
"Phasellus ipsum metus", "Phasellus ipsum metus; suscipit nec malesuada et, fermentum eu nulla. Vivamus id libero in ligula gravida tristique at sed nibh. Cras congue, risus posuere hendrerit pharetra, ante justo eleifend sem, vitae faucibus lacus neque rhoncus urna.",1123452333,55
"Nullam porta", "Nullam porta, nisl eu ornare rhoncus, dolor tortor scelerisque justo, non placerat mauris purus vel mauris.",1067356233,56
"", "Pellentesque eget ante sit amet turpis vestibulum posuere ut non ipsum. Suspendisse potenti. ",1202122311,57
"Pellentesque eget ante sit", "Pellentesque fringilla mi eu diam fermentum condimentum",1200010001,58
"Praesent pellentesque quam nec", "Praesent pellentesque quam nec ligula accumsan faucibus. Duis non nisi ante. Mauris eu ullamcorper urna.",1200000000,59
"Pellentesque habitant morbi tristique", "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus elit risus, interdum sed iaculis a, vehicula in eros.",1000000000,60
"Mauris lobortis luctus risus mattis luctus", "Mauris lobortis luctus risus mattis luctus. Praesent metus mi, euismod quis accumsan vel, placerat id dui. Donec sed erat vel arcu hendrerit hendrerit nec posuere metus! Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.",1202012011,61
"Mauris sed felis ut tortor gravida", "Mauris sed felis ut tortor gravida vestibulum vel ac enim!",1202123943,62
"Sed vehicula, ipsum non dignissim tristique", "Sed vehicula, ipsum non dignissim tristique, urna dui gravida elit, id tempor nunc eros sit amet ligula.",1202323423,63
"Duis consequat sagittis lectus id semper", "Duis consequat sagittis lectus id semper. Phasellus semper ante at leo faucibus venenatis.",1203124344,64
"Nunc malesuada convallis neque", "Nunc malesuada convallis neque ac tincidunt. Etiam hendrerit mi at arcu bibendum eget viverra mi fringilla.",1202334234,65
"Donec et ante mi?", "Donec et ante mi? Nullam auctor suscipit nibh quis dignissim. Etiam non lorem eros, vel auctor quam.",1205646554,66
"Cras consequat", "Cras consequat, ligula quis pulvinar ultrices, dui sem venenatis leo, et dignissim ante mi vitae ipsum.",1202345600,67
"Suspendisse convallis tempor adipiscing", "Suspendisse convallis tempor adipiscing. Vivamus ut ullamcorper nulla.",1202203200,68
"Vestibulum ante ipsum primis in faucibus orci luctus", "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.",1202202300,69
"Maecenas consectetur quam ac risus iaculis pharetra", "Maecenas consectetur quam ac risus iaculis pharetra. Nam metus velit, mattis ac interdum ac, volutpat et diam.",1202323430,70
"Ut odio massa", "Ut odio massa, luctus aliquam pretium non, imperdiet quis risus.",1202200000,71
"Curabitur sapien neque", "Curabitur sapien neque, elementum nec iaculis non, commodo nec arcu.",1206000000,72
"Duis a arcu felis", "Duis a arcu felis, id tristique lectus. Nullam porta mi vitae erat suscipit vulputate?",1202906458,73
"Cras sollicitudin lobortis rutrum", "Cras sollicitudin lobortis rutrum. Nunc vitae nisl nec orci laoreet cursus.",1204534599,74
"Cras fringilla commodo posuere", "Cras fringilla commodo posuere. Nam sed justo dolor, vel pharetra odio.",1205345344,75
"In fringilla erat et mi consequat convallis", "In fringilla erat et mi consequat convallis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.",1202455443,76
"Aliquam in tellus nibh", "Aliquam in tellus nibh, ut pharetra metus. Fusce tempor, lectus eget ultrices sagittis, ipsum est sagittis urna, vel porttitor magna sem ac diam.",1207566663,77
"Nunc gravida, leo sed faucibus viverra", "Nunc gravida, leo sed faucibus viverra, orci sapien mollis tellus, sit amet venenatis odio augue eu nunc.",1203453245,78
"Vestibulum tristique sodales arcu", "Vestibulum tristique sodales arcu, nec dignissim mauris rutrum et.",1202230010,79
"Praesent porttitor viverra rhoncus", "Praesent porttitor viverra rhoncus! Nulla malesuada elit et diam semper quis vehicula metus euismod.",1209200010,80
Can't render this file because it contains an unexpected character in line 5 and column 16.

View File

@@ -0,0 +1,10 @@
Title,Body,published,GUID
"Ut wisi enim ad minim veniam", "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.",205200720,2
"Duis autem vel eum iriure dolor", "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.",428112720,3
"Nam liber tempor", "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.",1151766000,1
Typi non habent"", "Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem.",1256326995,4
"Lorem ipsum","Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",1251936720,1
"Investigationes demonstraverunt", "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",946702800,5
"Claritas est etiam", "Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum.",438112720,6
"Mirum est notare", "Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima.",1151066000,7
"Eodem modo typi", "Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.",1201936720,8
Can't render this file because it contains an unexpected character in line 5 and column 16.

View File

@@ -0,0 +1,78 @@
<?php
/**
* @file
* Result of nodes.csv file parsed by ParserCSV.inc
*/
$control_result = array (
0 =>
array (
0 => 'Title',
1 => 'Body',
2 => 'published',
3 => 'GUID',
),
1 =>
array (
0 => 'Ut wisi enim ad minim veniam',
1 => ' Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.',
2 => '205200720',
3 => '2',
),
2 =>
array (
0 => 'Duis autem vel eum iriure dolor',
1 => ' Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.',
2 => '428112720',
3 => '3',
),
3 =>
array (
0 => 'Nam liber tempor',
1 => ' Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.',
2 => '1151766000',
3 => '1',
),
4 =>
array (
0 => 'Typi non habent',
1 => ' Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem.',
2 => '1256326995',
3 => '4',
),
5 =>
array (
0 => 'Lorem ipsum',
1 => 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.',
2 => '1251936720',
3 => '1',
),
6 =>
array (
0 => 'Investigationes demonstraverunt',
1 => ' Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.',
2 => '946702800',
3 => '5',
),
7 =>
array (
0 => 'Claritas est etiam',
1 => ' Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum.',
2 => '438112720',
3 => '6',
),
8 =>
array (
0 => 'Mirum est notare',
1 => ' Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima.',
2 => '1151066000',
3 => '7',
),
9 =>
array (
0 => 'Eodem modo typi',
1 => ' Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.',
2 => '1201936720',
3 => '8',
),
);

View File

@@ -0,0 +1,10 @@
Title Body published GUID
"Ut wisi enim ad minim veniam" "Ut wisi enim ad minim veniam quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat." 205200720 2
"Duis autem vel eum iriure dolor" "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi." 428112720 3
"Nam liber tempor" "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum." 1151766000 1
Typi non habent"" "Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem." 1256326995 4
"Lorem ipsum" "Lorem ipsum dolor sit amet consectetuer adipiscing elit sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat." 1251936720 1
"Investigationes demonstraverunt" "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius." 946702800 5
"Claritas est etiam" "Claritas est etiam processus dynamicus qui sequitur mutationem consuetudium lectorum." 438112720 6
"Mirum est notare" "Mirum est notare quam littera gothica quam nunc putamus parum claram anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima." 1151066000 7
"Eodem modo typi" "Eodem modo typi qui nunc nobis videntur parum clari fiant sollemnes in futurum." 1201936720 8
Can't render this file because it contains an unexpected character in line 5 and column 16.

View File

@@ -0,0 +1,10 @@
Title,Body,published,GUID
"Ut wisi enim ad minim veniam", "CHANGE Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.",205200720,2
"Duis autem vel eum CHANGE iriure dolor", "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.",428112720,3
"Nam liber tempor", "Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum.",1151766000,1
Typi non habent"", "Typi CHANGE non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem.",1256326995,4
"Lorem ipsum","Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.",1251936720,1
"Investigationes demonstraverunt", "Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius.",946702800,5
"Claritas CHANGE est etiam", "Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum.",438112720,6
"Mirum est notare", "Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima.",1151066000,7
"Eodem modo typi", "Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.",1201936720,8
Can't render this file because it contains an unexpected character in line 5 and column 16.

View File

@@ -0,0 +1,10 @@
Title,GUID,path
pathauto1,1,path1
pathauto2,2,path2
pathauto3,3,path3
pathauto4,4,path4
pathauto5,5,path5
pathauto6,6,path6
pathauto7,7,path7
pathauto8,8,path8
pathauto9,9,path9
1 Title GUID path
2 pathauto1 1 path1
3 pathauto2 2 path2
4 pathauto3 3 path3
5 pathauto4 4 path4
6 pathauto5 5 path5
7 pathauto6 6 path6
8 pathauto7 7 path7
9 pathauto8 8 path8
10 pathauto9 9 path9

View File

@@ -0,0 +1,3 @@
name,mail,color,letter
magna,auctor@tortor.com,red,alpha
rhoncus,rhoncus@habitasse.org,blue,beta
1 name mail color letter
2 magna auctor@tortor.com red alpha
3 rhoncus rhoncus@habitasse.org blue beta

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://www.example.com/</loc>
<lastmod>2005-01-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://www.example.com/catalog?item=12&amp;desc=vacation_hawaii</loc>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>http://www.example.com/catalog?item=73&amp;desc=vacation_new_zealand</loc>
<lastmod>2004-12-23</lastmod>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>http://www.example.com/catalog?item=74&amp;desc=vacation_newfoundland</loc>
<lastmod>2004-12-23T18:00:15+00:00</lastmod>
<priority>0.3</priority>
</url>
<url>
<loc>http://www.example.com/catalog?item=83&amp;desc=vacation_usa</loc>
<lastmod>2004-11-23</lastmod>
</url>
</urlset>

View File

@@ -0,0 +1,6 @@
name,mail,since,password
Morticia,morticia@example.com,1244347500,mort
Fester,fester@example.com,1241865600,fest
Gomez,gomez@example.com,1228572000,gome
Wednesday,wednesdayexample.com,1228347137,wedn
Pugsley,pugsley@example,1228260225,pugs
1 name mail since password
2 Morticia morticia@example.com 1244347500 mort
3 Fester fester@example.com 1241865600 fest
4 Gomez gomez@example.com 1228572000 gome
5 Wednesday wednesdayexample.com 1228347137 wedn
6 Pugsley pugsley@example 1228260225 pugs

View File

@@ -0,0 +1,42 @@
<?php
/**
* @file
* Tests for FeedsDateTime class.
*/
/**
* Test FeedsDateTime class.
*
* Using DrupalWebTestCase as DrupalUnitTestCase is broken in SimpleTest 2.8.
* Not inheriting from Feeds base class as ParserCSV should be moved out of
* Feeds at some time.
*/
class FeedsDateTimeTest extends FeedsWebTestCase {
protected $profile = 'testing';
public static function getInfo() {
return array(
'name' => 'FeedsDateTime unit tests',
'description' => 'Unit tests for Feeds date handling.',
'group' => 'Feeds',
);
}
public function setUp() {
parent::setUp();
module_load_include('inc', 'feeds' , 'plugins/FeedsParser');
}
/**
* Dispatch tests, only use one entry point method testX to save time.
*/
public function test() {
$date = new FeedsDateTime('2010-20-12');
$this->assertTrue(is_numeric($date->format('U')));
$date = new FeedsDateTime('created');
$this->assertTrue(is_numeric($date->format('U')));
$date = new FeedsDateTime('12/3/2009 20:00:10');
$this->assertTrue(is_numeric($date->format('U')));
}
}

View File

@@ -0,0 +1,70 @@
<?php
/**
* @file
* File fetcher tests.
*/
/**
* File fetcher test class.
*/
class FeedsFileFetcherTestCase extends FeedsWebTestCase {
public static function getInfo() {
return array(
'name' => 'File fetcher',
'description' => 'Tests for file fetcher plugin.',
'group' => 'Feeds',
);
}
/**
* Test scheduling on cron.
*/
public function test() {
// Set up an importer.
$this->createImporterConfiguration('Node import', 'node');
// Set and configure plugins and mappings.
$edit = array(
'content_type' => '',
);
$this->drupalPost('admin/structure/feeds/node/settings', $edit, 'Save');
$this->setPlugin('node', 'FeedsFileFetcher');
$this->setPlugin('node', 'FeedsCSVParser');
$mappings = array(
'0' => array(
'source' => 'title',
'target' => 'title',
),
);
$this->addMappings('node', $mappings);
// Straight up upload is covered in other tests, focus on direct mode
// and file batching here.
$this->setSettings('node', 'FeedsFileFetcher', array('direct' => TRUE));
// Verify that invalid paths are not accepted.
foreach (array('private://', '/tmp/') as $path) {
$edit = array(
'feeds[FeedsFileFetcher][source]' => $path,
);
$this->drupalPost('import/node', $edit, t('Import'));
$this->assertText("File needs to reside within the site's file directory, its path needs to start with public://.");
$count = db_query("SELECT COUNT(*) FROM {feeds_source} WHERE feed_nid = 0")->fetchField();
$this->assertEqual($count, 0);
}
// Verify batching through directories.
// Copy directory of files.
$dir = 'public://batchtest';
$this->copyDir($this->absolutePath() . '/tests/feeds/batch', $dir);
// Ingest directory of files. Set limit to 5 to force processor to batch,
// too.
variable_set('feeds_process_limit', 5);
$edit = array(
'feeds[FeedsFileFetcher][source]' => $dir,
);
$this->drupalPost('import/node', $edit, t('Import'));
$this->assertText('Created 18 nodes');
}
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* @file
* Helper class with auxiliary functions for feeds mapper module tests.
*/
/**
* Base class for implementing Feeds Mapper test cases.
*/
class FeedsMapperTestCase extends FeedsWebTestCase {
// A lookup map to select the widget for each field type.
private static $field_widgets = array(
'date' => 'date_text',
'datestamp' => 'date_text',
'datetime' => 'date_text',
'number_decimal' => 'number',
'email' => 'email_textfield',
'emimage' => 'emimage_textfields',
'emaudio' => 'emaudio_textfields',
'filefield' => 'filefield_widget',
'image' => 'imagefield_widget',
'link_field' => 'link_field',
'number_float' => 'number',
'number_integer' => 'number',
'nodereference' => 'nodereference_select',
'text' => 'text_textfield',
'userreference' => 'userreference_select',
);
/**
* Assert that a form field for the given field with the given value
* exists in the current form.
*
* @param $field_name
* The name of the field.
* @param $value
* The (raw) value expected for the field.
* @param $index
* The index of the field (for q multi-valued field).
*
* @see FeedsMapperTestCase::getFormFieldsNames()
* @see FeedsMapperTestCase::getFormFieldsValues()
*/
protected function assertNodeFieldValue($field_name, $value, $index = 0) {
$names = $this->getFormFieldsNames($field_name, $index);
$values = $this->getFormFieldsValues($field_name, $value);
foreach ($names as $k => $name) {
$value = $values[$k];
$this->assertFieldByName($name, $value, t('Found form field %name for %field_name with the expected value.', array('%name' => $name, '%field_name' => $field_name)));
}
}
/**
* Returns the form fields names for a given CCK field. Default implementation
* provides support for a single form field with the following name pattern
* <code>"field_{$field_name}[{$index}][value]"</code>
*
* @param $field_name
* The name of the CCK field.
* @param $index
* The index of the field (for q multi-valued field).
*
* @return
* An array of form field names.
*/
protected function getFormFieldsNames($field_name, $index) {
return array("field_{$field_name}[und][{$index}][value]");
}
/**
* Returns the form fields values for a given CCK field. Default implementation
* returns a single element array with $value casted to a string.
*
* @param $field_name
* The name of the CCK field.
* @param $value
* The (raw) value expected for the CCK field.
* @return An array of form field values.
*/
protected function getFormFieldsValues($field_name, $value) {
return array((string)$value);
}
/**
* Create a new content-type, and add a field to it. Mostly copied from
* cck/tests/content.crud.test ContentUICrud::testAddFieldUI
*
* @param $settings
* (Optional) An array of settings to pass through to
* drupalCreateContentType().
* @param $fields
* (Optional) an keyed array of $field_name => $field_type used to add additional
* fields to the new content type.
*
* @return
* The machine name of the new content type.
*
* @see DrupalWebTestCase::drupalCreateContentType()
*/
final protected function createContentType(array $settings = array(), array $fields = array()) {
$type = $this->drupalCreateContentType($settings);
$typename = $type->type;
$admin_type_url = 'admin/structure/types/manage/' . str_replace('_', '-', $typename);
// Create the fields
foreach ($fields as $field_name => $options) {
if (is_string($options)) {
$options = array('type' => $options);
}
$field_type = isset($options['type']) ? $options['type'] : 'text';
$field_widget = isset($options['widget']) ? $options['widget'] : $this->selectFieldWidget($field_name, $field_type);
$this->assertTrue($field_widget !== NULL, "Field type $field_type supported");
$label = $field_name . '_' . $field_type . '_label';
$edit = array(
'fields[_add_new_field][label]' => $label,
'fields[_add_new_field][field_name]' => $field_name,
'fields[_add_new_field][type]' => $field_type,
'fields[_add_new_field][widget_type]' => $field_widget,
);
$this->drupalPost($admin_type_url . '/fields', $edit, 'Save');
// (Default) Configure the field.
$edit = isset($options['settings']) ? $options['settings'] : array();
$this->drupalPost(NULL, $edit, 'Save field settings');
$this->assertText('Updated field ' . $label . ' field settings.');
$edit = isset($options['instance_settings']) ? $options['instance_settings'] : array();
$this->drupalPost(NULL, $edit, 'Save settings');
$this->assertText('Saved ' . $label . ' configuration.');
}
return $typename;
}
/**
* Select the widget for the field. Default implementation provides widgets
* for Date, Number, Text, Node reference, User reference, Email, Emfield,
* Filefield, Image, and Link.
*
* Extracted as a method to allow test implementations to add widgets for
* the tested CCK field type(s). $field_name allow to test the same
* field type with different widget (is this useful ?)
*
* @param $field_name
* The name of the field.
* @param $field_type
* The CCK type of the field.
*
* @return
* The widget for this field, or NULL if the field_type is not
* supported by this test class.
*/
protected function selectFieldWidget($field_name, $field_type) {
$field_widgets = FeedsMapperTestCase::$field_widgets;
return isset($field_widgets[$field_type]) ? $field_widgets[$field_type] : NULL;
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* @file
* Test cases for Feeds mapping configuration form.
*/
/**
* Class for testing basic Feeds ajax mapping configurtaion form behavior.
*/
class FeedsMapperConfigTestCase extends FeedsMapperTestCase {
public static function getInfo() {
return array(
'name' => 'Mapper: Config',
'description' => 'Test the mapper configuration UI.',
'group' => 'Feeds',
);
}
public function setUp() {
parent::setUp(array('feeds_tests'));
}
/**
* Basic test of mapping configuration.
*/
public function test() {
// Create importer configuration.
$this->createImporterConfiguration();
$this->addMappings('syndication', array(
array(
'source' => 'url',
'target' => 'test_target',
),
));
// Click gear to get form.
$this->drupalPostAJAX(NULL, array(), 'mapping_settings_edit_0');
// Set some settings.
$edit = array(
'config[0][settings][checkbox]' => 1,
'config[0][settings][textfield]' => 'Some text',
'config[0][settings][textarea]' => 'Textarea value: Didery dofffffffffffffffffffffffffffffffffffff',
'config[0][settings][radios]' => 'option1',
'config[0][settings][select]' => 'option4',
);
$this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_0');
// Click Save.
$this->drupalPost(NULL, array(), t('Save'));
// Reload.
$this->drupalGet('admin/structure/feeds/syndication/mapping');
// See if our settings were saved.
$this->assertText('Checkbox active.');
$this->assertText('Textfield value: Some text');
$this->assertText('Textarea value: Didery dofffffffffffffffffffffffffffffffffffff');
$this->assertText('Radios value: Option 1');
$this->assertText('Select value: Another One');
// Check that settings are in db.
$config = unserialize(db_query("SELECT config FROM {feeds_importer} WHERE id='syndication'")->fetchField());
$settings = $config['processor']['config']['mappings'][0];
$this->assertEqual($settings['checkbox'], 1);
$this->assertEqual($settings['textfield'], 'Some text');
$this->assertEqual($settings['textarea'], 'Textarea value: Didery dofffffffffffffffffffffffffffffffffffff');
$this->assertEqual($settings['radios'], 'option1');
$this->assertEqual($settings['select'], 'option4');
// Check that form validation works.
// Click gear to get form.
$this->drupalPostAJAX(NULL, array(), 'mapping_settings_edit_0');
// Set some settings.
$edit = array(
// Required form item.
'config[0][settings][textfield]' => '',
);
$this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_0');
$this->assertText('A text field field is required.');
$this->drupalPost(NULL, array(), t('Save'));
// Reload.
$this->drupalGet('admin/structure/feeds/syndication/mapping');
// Value has not changed.
$this->assertText('Textfield value: Some text');
// Check that multiple mappings work.
$this->addMappings('syndication', array(
1 => array(
'source' => 'url',
'target' => 'test_target',
),
));
$this->assertText('Checkbox active.');
$this->assertText('Checkbox inactive.');
// Click gear to get form.
$this->drupalPostAJAX(NULL, array(), 'mapping_settings_edit_1');
// Set some settings.
$edit = array(
'config[1][settings][textfield]' => 'Second mapping text',
);
$this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_1');
// Click Save.
$this->drupalPost(NULL, array(), t('Save'));
// Reload.
$this->drupalGet('admin/structure/feeds/syndication/mapping');
$this->assertText('Checkbox active.');
$this->assertText('Checkbox inactive.');
$this->assertText('Second mapping text');
}
}

View File

@@ -0,0 +1,100 @@
<?php
/**
* @file
* Test case for CCK date field mapper mappers/date.inc.
*/
/**
* Class for testing Feeds <em>content</em> mapper.
*
* @todo: Add test method iCal
* @todo: Add test method for end date
*/
class FeedsMapperDateTestCase extends FeedsMapperTestCase {
public static function getInfo() {
return array(
'name' => 'Mapper: Date',
'description' => 'Test Feeds Mapper support for CCK Date fields.',
'group' => 'Feeds',
'dependencies' => array('date'),
);
}
public function setUp() {
parent::setUp(array('date_api', 'date'));
variable_set('date_default_timezone', 'UTC');
}
/**
* Basic test loading a single entry CSV file.
*/
public function test() {
$this->drupalGet('admin/config/regional/settings');
// Create content type.
$typename = $this->createContentType(array(), array(
'date' => 'date',
'datestamp' => 'datestamp',
//'datetime' => 'datetime', // REMOVED because the field is broken ATM.
));
// Create and configure importer.
$this->createImporterConfiguration('Date RSS', 'daterss');
$this->setSettings('daterss', NULL, array('content_type' => '', 'import_period' => FEEDS_SCHEDULE_NEVER));
$this->setPlugin('daterss', 'FeedsFileFetcher');
$this->setPlugin('daterss', 'FeedsSyndicationParser');
$this->setSettings('daterss', 'FeedsNodeProcessor', array('content_type' => $typename));
$this->addMappings('daterss', array(
0 => array(
'source' => 'title',
'target' => 'title',
),
1 => array(
'source' => 'description',
'target' => 'body',
),
2 => array(
'source' => 'timestamp',
'target' => 'field_date:start',
),
3 => array(
'source' => 'timestamp',
'target' => 'field_datestamp:start',
),
));
$edit = array(
'allowed_extensions' => 'rss2',
);
$this->drupalPost('admin/structure/feeds/daterss/settings/FeedsFileFetcher', $edit, 'Save');
// Import CSV file.
$this->importFile('daterss', $this->absolutePath() . '/tests/feeds/googlenewstz.rss2');
$this->assertText('Created 6 nodes');
// Check the imported nodes.
$values = array(
'01/06/2010 - 19:26',
'01/06/2010 - 10:21',
'01/06/2010 - 13:42',
'01/06/2010 - 06:05',
'01/06/2010 - 11:26',
'01/07/2010 - 00:26',
);
for ($i = 1; $i <= 6; $i++) {
$this->drupalGet("node/$i/edit");
$this->assertNodeFieldValue('date', $values[$i-1]);
$this->assertNodeFieldValue('datestamp', $values[$i-1]);
}
}
protected function getFormFieldsNames($field_name, $index) {
if (in_array($field_name, array('date', 'datetime', 'datestamp'))) {
return array("field_{$field_name}[und][{$index}][value][date]");
}
else {
return parent::getFormFieldsNames($field_name, $index);
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* @file
* Test case for simple CCK field mapper mappers/content.inc.
*/
/**
* Class for testing Feeds field mapper.
*/
class FeedsMapperFieldTestCase extends FeedsMapperTestCase {
public static function getInfo() {
return array(
'name' => 'Mapper: Fields',
'description' => 'Test Feeds Mapper support for fields.',
'group' => 'Feeds',
);
}
public function setUp() {
parent::setUp(array('number'));
}
/**
* Basic test loading a double entry CSV file.
*/
function test() {
// Create content type.
$typename = $this->createContentType(array(), array(
'alpha' => 'text',
'beta' => 'number_integer',
'gamma' => 'number_decimal',
'delta' => 'number_float',
));
// Create and configure importer.
$this->createImporterConfiguration('Content CSV', 'csv');
$this->setSettings('csv', NULL, array('content_type' => '', 'import_period' => FEEDS_SCHEDULE_NEVER));
$this->setPlugin('csv', 'FeedsFileFetcher');
$this->setPlugin('csv', 'FeedsCSVParser');
$this->setSettings('csv', 'FeedsNodeProcessor', array('content_type' => $typename));
$this->addMappings('csv', array(
0 => array(
'source' => 'title',
'target' => 'title',
),
1 => array(
'source' => 'created',
'target' => 'created',
),
2 => array(
'source' => 'body',
'target' => 'body',
),
3 => array(
'source' => 'alpha',
'target' => 'field_alpha',
),
4 => array(
'source' => 'beta',
'target' => 'field_beta',
),
5 => array(
'source' => 'gamma',
'target' => 'field_gamma',
),
6 => array(
'source' => 'delta',
'target' => 'field_delta',
),
));
// Import CSV file.
$this->importFile('csv', $this->absolutePath() . '/tests/feeds/content.csv');
$this->assertText('Created 2 nodes');
// Check the two imported files.
$this->drupalGet('node/1/edit');
$this->assertNodeFieldValue('alpha', 'Lorem');
$this->assertNodeFieldValue('beta', '42');
$this->assertNodeFieldValue('gamma', '4.20');
$this->assertNodeFieldValue('delta', '3.14159');
$this->drupalGet('node/2/edit');
$this->assertNodeFieldValue('alpha', 'Ut wisi');
$this->assertNodeFieldValue('beta', '32');
$this->assertNodeFieldValue('gamma', '1.20');
$this->assertNodeFieldValue('delta', '5.62951');
}
}

View File

@@ -0,0 +1,180 @@
<?php
/**
* @file
* Test case for Filefield mapper mappers/filefield.inc.
*/
/**
* Class for testing Feeds file mapper.
*
* @todo Add a test for enclosures using a local file that is
* a) in place and that
* b) needs to be copied from one location to another.
*/
class FeedsMapperFileTestCase extends FeedsMapperTestCase {
public static function getInfo() {
return array(
'name' => 'Mapper: File field',
'description' => 'Test Feeds Mapper support for file fields. <strong>Requires SimplePie library</strong>.',
'group' => 'Feeds',
);
}
/**
* Basic test loading a single entry CSV file.
*/
public function test() {
// If this is unset (or FALSE) http_request.inc will use curl, and will generate a 404
// for this feel url provided by feeds_tests. However, if feeds_tests was enabled in your
// site before running the test, it will work fine. Since it is truly screwy, lets just
// force it to use drupal_http_request for this test case.
variable_set('feeds_never_use_curl', TRUE);
variable_set('clean_url', TRUE);
// Only download simplepie if the plugin doesn't already exist somewhere.
// People running tests locally might have it.
if (!feeds_simplepie_exists()) {
$this->downloadExtractSimplePie('1.3');
$this->assertTrue(feeds_simplepie_exists());
// Reset all the caches!
$this->resetAll();
}
$typename = $this->createContentType(array(), array('files' => 'file'));
// 1) Test mapping remote resources to file field.
// Create importer configuration.
$this->createImporterConfiguration();
$this->setPlugin('syndication', 'FeedsSimplePieParser');
$this->setSettings('syndication', 'FeedsNodeProcessor', array('content_type' => $typename));
$this->addMappings('syndication', array(
0 => array(
'source' => 'title',
'target' => 'title'
),
1 => array(
'source' => 'timestamp',
'target' => 'created'
),
2 => array(
'source' => 'enclosures',
'target' => 'field_files'
),
));
$nid = $this->createFeedNode('syndication', $GLOBALS['base_url'] . '/testing/feeds/flickr.xml');
$this->assertText('Created 5 nodes');
$files = $this->_testFiles();
$entities = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('id', 'syndication')
->execute();
foreach ($entities as $entity) {
$this->drupalGet('node/' . $entity->entity_id . '/edit');
$f = new FeedsEnclosure(array_shift($files), NULL);
$this->assertText($f->getLocalValue());
}
// 2) Test mapping local resources to file field.
// Copy directory of files, CSV file expects them in public://images, point
// file field to a 'resources' directory. Feeds should copy files from
// images/ to resources/ on import.
$this->copyDir($this->absolutePath() . '/tests/feeds/assets', 'public://images');
$edit = array(
'instance[settings][file_directory]' => 'resources',
);
$this->drupalPost('admin/structure/types/manage/' . $typename . '/fields/field_files', $edit, t('Save settings'));
// Create a CSV importer configuration.
$this->createImporterConfiguration('Node import from CSV', 'node');
$this->setPlugin('node', 'FeedsCSVParser');
$this->setSettings('node', 'FeedsNodeProcessor', array('content_type' => $typename));
$this->addMappings('node', array(
0 => array(
'source' => 'title',
'target' => 'title'
),
1 => array(
'source' => 'file',
'target' => 'field_files'
),
));
$edit = array(
'content_type' => '',
);
$this->drupalPost('admin/structure/feeds/node/settings', $edit, 'Save');
// Import.
$edit = array(
'feeds[FeedsHTTPFetcher][source]' => $GLOBALS['base_url'] . '/testing/feeds/files.csv',
);
$this->drupalPost('import/node', $edit, 'Import');
$this->assertText('Created 5 nodes');
// Assert: files should be in resources/.
$files = $this->_testFiles();
$entities = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('id', 'node')
->execute();
foreach ($entities as $entity) {
$this->drupalGet('node/' . $entity->entity_id . '/edit');
$f = new FeedsEnclosure(array_shift($files), NULL);
$this->assertRaw('resources/' . $f->getUrlEncodedValue());
}
// 3) Test mapping of local resources, this time leave files in place.
$this->drupalPost('import/node/delete-items', array(), 'Delete');
// Setting the fields file directory to images will make copying files
// obsolete.
$edit = array(
'instance[settings][file_directory]' => 'images',
);
$this->drupalPost('admin/structure/types/manage/' . $typename . '/fields/field_files', $edit, t('Save settings'));
$edit = array(
'feeds[FeedsHTTPFetcher][source]' => $GLOBALS['base_url'] . '/testing/feeds/files.csv',
);
$this->drupalPost('import/node', $edit, 'Import');
$this->assertText('Created 5 nodes');
// Assert: files should be in images/ now.
$files = $this->_testFiles();
$entities = db_select('feeds_item')
->fields('feeds_item', array('entity_id'))
->condition('id', 'node')
->execute();
foreach ($entities as $entity) {
$this->drupalGet('node/' . $entity->entity_id . '/edit');
$f = new FeedsEnclosure(array_shift($files), NULL);
$this->assertRaw('images/' . $f->getUrlEncodedValue());
}
// Deleting all imported items will delete the files from the images/ dir.
// @todo: for some reason the first file does not get deleted.
// $this->drupalPost('import/node/delete-items', array(), 'Delete');
// foreach ($this->_testFiles() as $file) {
// $this->assertFalse(is_file("public://images/$file"));
// }
}
/**
* Lists test files.
*/
public function _testFiles() {
return array('tubing.jpeg', 'foosball.jpeg', 'attersee.jpeg', 'hstreet.jpeg', 'la fayette.jpeg');
}
/**
* Handle file field widgets.
*/
public function selectFieldWidget($fied_name, $field_type) {
if ($field_type == 'file') {
return 'file_generic';
}
else {
return parent::selectFieldWidget($fied_name, $field_type);
}
}
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* @file
* Test case for CCK link mapper mappers/date.inc.
*/
/**
* Class for testing Feeds <em>link</em> mapper.
*/
class FeedsMapperLinkTestCase extends FeedsMapperTestCase {
public static function getInfo() {
return array(
'name' => 'Mapper: Link',
'description' => 'Test Feeds Mapper support for Link fields.',
'group' => 'Feeds',
'dependencies' => array('link'),
);
}
public function setUp() {
parent::setUp(array('link'));
}
/**
* Basic test loading a single entry CSV file.
*/
public function test() {
$static_title = $this->randomName();
// Create content type.
$typename = $this->createContentType(array(), array(
'alpha' => array(
'type' => 'link_field',
'instance_settings' => array(
'instance[settings][title]' => 'required',
),
),
'beta' => array(
'type' => 'link_field',
'instance_settings' => array(
'instance[settings][title]' => 'none',
),
),
'gamma' => array(
'type' => 'link_field',
'instance_settings' => array(
'instance[settings][title]' => 'optional',
),
),
'omega' => array(
'type' => 'link_field',
'instance_settings' => array(
'instance[settings][title]' => 'value',
'instance[settings][title_value]' => $static_title,
),
),
));
// Create importer configuration.
$this->createImporterConfiguration(); //Create a default importer configuration
$this->setSettings('syndication', 'FeedsNodeProcessor', array('content_type' => $typename)); //Processor settings
$this->addMappings('syndication', array(
0 => array(
'source' => 'title',
'target' => 'title'
),
1 => array(
'source' => 'timestamp',
'target' => 'created'
),
2 => array(
'source' => 'description',
'target' => 'body'
),
3 => array(
'source' => 'url',
'target' => 'field_alpha:url'
),
4 => array(
'source' => 'title',
'target' => 'field_alpha:title'
),
5 => array(
'source' => 'url',
'target' => 'field_beta:url'
),
6 => array(
'source' => 'url',
'target' => 'field_gamma:url'
),
7 => array(
'source' => 'title',
'target' => 'field_gamma:title'
),
8 => array(
'source' => 'url',
'target' => 'field_omega:url'
),
));
// Import RSS file.
$nid = $this->createFeedNode();
// Assert 10 items aggregated after creation of the node.
$this->assertText('Created 10 nodes');
// Edit the imported node.
$this->drupalGet('node/2/edit');
$url = 'http://developmentseed.org/blog/2009/oct/06/open-atrium-translation-workflow-two-way-updating';
$title = 'Open Atrium Translation Workflow: Two Way Translation Updates';
$this->assertNodeFieldValue('alpha', array('url' => $url, 'static' => $title));
$this->assertNodeFieldValue('beta', array('url' => $url));
$this->assertNodeFieldValue('gamma', array('url' => $url, 'static' => $title));
$this->assertNodeFieldValue('omega', array('url' => $url, 'static' => $static_title));
// Test the static title.
$this->drupalGet('node/2');
$this->assertText($static_title, 'Static title link found.');
}
/**
* Override parent::getFormFieldsNames().
*/
protected function getFormFieldsNames($field_name, $index) {
if (in_array($field_name, array('alpha', 'beta', 'gamma', 'omega'))) {
$fields = array("field_{$field_name}[und][{$index}][url]");
if (in_array($field_name, array('alpha', 'gamma'))) {
$fields[] = "field_{$field_name}[und][{$index}][title]";
}
return $fields;
}
else {
return parent::getFormFieldsNames($field_name, $index);
}
}
/**
* Override parent::getFormFieldsValues().
*/
protected function getFormFieldsValues($field_name, $value) {
if (in_array($field_name, array('alpha', 'beta', 'gamma', 'omega'))) {
if (!is_array($value)) {
$value = array('url' => $value);
}
$values = array($value['url']);
if (in_array($field_name, array('alpha', 'gamma'))) {
$values[] = isset($value['title']) ? $value['title'] : '';
}
return $values;
}
else {
return parent::getFormFieldsValues($field_name, $index);
}
}
}

Some files were not shown because too many files have changed in this diff Show More