├── .gitignore ├── .shipit ├── Changes ├── MANIFEST ├── MANIFEST.SKIP ├── Makefile.PL ├── bin ├── dump_changesets.idx ├── dump_record.idx ├── prophet ├── run_test_yml.pl └── taste_recipe ├── doc ├── foreign-replicas ├── glossary ├── jesse_test_env_setup ├── luid ├── merging-and-conflicts ├── notes-on-merging ├── repository-layout ├── tab-completion ├── todo └── web_form_handling ├── etc ├── prophet.bash └── prophet.zsh ├── inc └── Module │ ├── AutoInstall.pm │ ├── Install.pm │ └── Install │ ├── AutoInstall.pm │ ├── Base.pm │ ├── Can.pm │ ├── Fetch.pm │ ├── Include.pm │ ├── Makefile.pm │ ├── Metadata.pm │ ├── Share.pm │ ├── Win32.pm │ └── WriteAll.pm ├── lib ├── Prophet.pm └── Prophet │ ├── App.pm │ ├── CLI.pm │ ├── CLI │ ├── CollectionCommand.pm │ ├── Command.pm │ ├── Command │ │ ├── Aliases.pm │ │ ├── Clone.pm │ │ ├── Config.pm │ │ ├── Create.pm │ │ ├── Delete.pm │ │ ├── Export.pm │ │ ├── History.pm │ │ ├── Info.pm │ │ ├── Init.pm │ │ ├── Log.pm │ │ ├── Merge.pm │ │ ├── Mirror.pm │ │ ├── Publish.pm │ │ ├── Pull.pm │ │ ├── Push.pm │ │ ├── Search.pm │ │ ├── Server.pm │ │ ├── Settings.pm │ │ ├── Shell.pm │ │ ├── Show.pm │ │ └── Update.pm │ ├── Dispatcher.pm │ ├── Dispatcher │ │ ├── Rule.pm │ │ └── Rule │ │ │ └── RecordId.pm │ ├── MirrorCommand.pm │ ├── Parameters.pm │ ├── ProgressBar.pm │ ├── PublishCommand.pm │ ├── RecordCommand.pm │ └── TextEditorCommand.pm │ ├── CLIContext.pm │ ├── Change.pm │ ├── ChangeSet.pm │ ├── Collection.pm │ ├── Config.pm │ ├── Conflict.pm │ ├── ConflictingChange.pm │ ├── ConflictingPropChange.pm │ ├── ContentAddressedStore.pm │ ├── DatabaseSetting.pm │ ├── FilesystemReplica.pm │ ├── ForeignReplica.pm │ ├── Manual.pod │ ├── Meta │ └── Types.pm │ ├── PropChange.pm │ ├── Record.pm │ ├── Replica.pm │ ├── Replica │ ├── FS │ │ └── Backend │ │ │ ├── File.pm │ │ │ ├── LWP.pm │ │ │ └── SSH.pm │ ├── file.pm │ ├── http.pm │ ├── prophet.pm │ ├── prophet_cache.pm │ └── sqlite.pm │ ├── ReplicaExporter.pm │ ├── ReplicaFeedExporter.pm │ ├── Resolver.pm │ ├── Resolver │ ├── AlwaysSource.pm │ ├── AlwaysTarget.pm │ ├── Failed.pm │ ├── Fixup │ │ └── MissingSourceOldValues.pm │ ├── FromResolutionDB.pm │ ├── IdenticalChanges.pm │ └── Prompt.pm │ ├── Server.pm │ ├── Server │ ├── Controller.pm │ ├── Dispatcher.pm │ ├── View.pm │ ├── ViewHelpers.pm │ └── ViewHelpers │ │ ├── Function.pm │ │ ├── HiddenParam.pm │ │ ├── ParamFromFunction.pm │ │ └── Widget.pm │ ├── Test.pm │ ├── Test │ ├── Arena.pm │ ├── Editor.pm │ └── Participant.pm │ ├── UUIDGenerator.pm │ ├── Util.pm │ └── Web │ ├── Field.pm │ ├── FunctionResult.pm │ ├── Menu.pm │ └── Result.pm ├── share └── web │ └── static │ ├── jquery │ ├── css │ │ ├── indicator.gif │ │ ├── jquery.autocomplete.css │ │ ├── superfish-navbar.css │ │ ├── superfish-vertical.css │ │ ├── superfish.css │ │ └── tablesorter │ │ │ ├── asc.gif │ │ │ ├── bg.gif │ │ │ ├── desc.gif │ │ │ └── style.css │ ├── images │ │ ├── arrows-cccccc.png │ │ ├── arrows-ffffff.png │ │ └── shadow.png │ └── js │ │ ├── hoverIntent.js │ │ ├── jquery-1.2.6.min.js │ │ ├── jquery-autocomplete.js │ │ ├── jquery.bgiframe.min.js │ │ ├── jquery.tablesorter.min.js │ │ ├── pretty.js │ │ ├── superfish.js │ │ └── supersubs.js │ └── yui │ └── css │ └── reset.css ├── t ├── Settings │ ├── bin │ │ └── settings │ ├── lib │ │ └── App │ │ │ ├── Settings.pm │ │ │ └── Settings │ │ │ ├── Bug.pm │ │ │ ├── CLI.pm │ │ │ └── Test.pm │ └── t │ │ ├── database-settings-editor.t │ │ └── sync-database-settings.t ├── WebToy │ ├── bin │ │ └── webtoy │ └── lib │ │ └── App │ │ ├── WebToy.pm │ │ └── WebToy │ │ ├── CLI.pm │ │ ├── Collection │ │ └── WikiPage.pm │ │ ├── Model │ │ └── WikiPage.pm │ │ └── Server │ │ ├── Dispatcher.pm │ │ └── View.pm ├── aliases.t ├── aliases_with_quotes.t ├── canonicalize.t ├── cli-arg-parsing.t ├── cli-arg-translation.t ├── cli.t ├── config.t ├── create-conflict.t ├── create.t ├── data │ ├── aliases.tmpl │ ├── settings-first.tmpl │ ├── settings-second.tmpl │ └── settings-third.tmpl ├── database-settings.t ├── default.t ├── delete-delete-conflict.t ├── edit.t ├── error.t ├── export.t ├── generalized_sync_n_merge.t ├── history.t ├── info.t ├── init.t ├── lib │ └── TestApp │ │ ├── Bug.pm │ │ ├── BugCatcher.pm │ │ ├── Bugs.pm │ │ └── ButterflyNet.pm ├── local_metadata.t ├── log.t ├── luid.t ├── malformed-url.t ├── non-conflicting-merge.t ├── publish-html.t ├── publish-pull.t ├── real-conflicting-merge.t ├── record-types.t ├── references.t ├── res-conflict-3.t ├── resty-server.t ├── search.t ├── simple-conflicting-merge.t ├── simple-push.t ├── sync-change-to-original-source.t ├── sync-delete-conflict.t ├── sync-ticket.t ├── sync_3party.t ├── test_app.conf ├── usage.t ├── use.t ├── util.t ├── validate.t └── validation.t └── xt ├── 01-dependencies.t ├── 99-pod-coverage.t └── 99-pod.t /.gitignore: -------------------------------------------------------------------------------- 1 | blib/ 2 | pm_to_blib 3 | Makefile 4 | Makefile.old 5 | *~ 6 | .prove 7 | META.yml 8 | cover_db 9 | inc/ 10 | -------------------------------------------------------------------------------- /.shipit: -------------------------------------------------------------------------------- 1 | # auto-generated shipit config file. 2 | steps = FindVersion, ChangeVersion, CheckChangeLog, DistTest, Commit, Tag, MakeDist, UploadCPAN 3 | 4 | git.tagpattern = %v 5 | # twitter.config = ~/.twitterrc 6 | 7 | # svn.tagpattern = MyProj-%v 8 | # svn.tagpattern = http://code.example.com/svn/tags/MyProj-%v 9 | 10 | # CheckChangeLog.files = ChangeLog, MyProj.CHANGES 11 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | 0.743 2 | * Fix test failures so install works again -- spang 3 | 4 | 0.742 5 | * Deal with Path::Dispatcher::Declarative being split from P::D --jesse 6 | 7 | 0.741 8 | 9 | * Actually ship all the javascript and css files for the web frontend. 10 | (Christine Spang) 11 | * bash and zsh completion, see doc/tab-completion for how to enable 12 | (Shawn Moore, Kevin Falcone) 13 | 14 | 0.74 15 | 16 | User-visible highlights for this release (not all commits are listed here): 17 | * Solve double-prompting for username/password in foreign syncs - Christine Spang 18 | * Fixes for Moose compatibility - Christine Spang 19 | * Unbreak with Mouse > 0.40 - Florian Ragwitz 20 | * Some code from SD had accidentially snuck into prophet. rafl++ for alerting me - Jesse Vincent 21 | * make menu links relative - Jesse Vincent 22 | * propagate "server" into child menus - Jesse Vincent 23 | * Announce project_name via Bonjour - Pedro Melo 24 | * allow apps to skip overriding dispatcher_class - Ruslan Zakirov 25 | * return that file doesn't exist only when lwp_get returns undef - Ruslan Zakirov 26 | * Add tab completion for prophet shell - Shawn Moore 27 | * check and store in config username and secret token - franck cuny 28 | * add --as to clone - franck cuny 29 | 30 | Thanks to the following people who contributed to this release: 31 | Alex Vandiver, Christine Spang, Florian Ragwitz, Jesse Vincent, Pedro Melo, 32 | Ruslan Zakirov, Shawn Moore, and franck cuny. 33 | 34 | 0.73 35 | 36 | * Reimplement alias expansion in terms of lists of argument words. - Nelson Elhage 37 | * First pass at improving UTF8 output in static web views - Jesse Vincent 38 | 39 | 0.72 Fri Sep 4 13:20:16 EDT 2009 40 | 41 | * fix sqlite replica: original_sequence_no can be 0 - sunnavy 42 | * add inc/ back, we should keep it in repo - sunnavy 43 | * Added a couple debugging tools - dump_changesets.idx dump_record.idx - Jesse Vincent 44 | * Prophet::CLI::RecordCommand now checks to make sure you've asked it to operate on a record that actually exists. - Jesse Vincent 45 | * Added a Prophet::Record API for "does this exist?" - Jesse Vincent 46 | * fixing old docs that were out of date - Jesse Vincent 47 | * Made sure that sqlite replicas userdata keys are always lowercase - Jesse Vincent 48 | * Made an "is this replica me?" query case insensitive. - Jesse Vincent 49 | * Prophet::App now has a friendly name for "was asked to characterize an undef replica" - Jesse Vincent 50 | * Prophet::FilesystemReplia's local metadata is now case insensitive - Jesse Vincent 51 | * Failing tests proving that local metadata isn't case insensitive - Jesse Vincent 52 | * more notes to the long and ugly alias value stuff parser - sunnavy 53 | * better alias value parse: to handle ' and " - sunnavy 54 | * add aliases test with quotes - sunnavy 55 | * Support var-args aliases. - Nelson Elhage 56 | * Improve argument expansion in aliases. - Nelson Elhage 57 | * Only expand aliases on word boundaries. - Nelson Elhage 58 | * Remove an unnecessary 'no strict "refs"'. - Nelson Elhage 59 | * Switch to UUID::Tiny 1.02 and remove our temporary fork - Christine Spang 60 | * Reload config after editing (needed in shell) - Christine Spang 61 | * Better error message when pushing to non-existant replica - Christine Spang 62 | * Web templates now default to utf8 - Jesse Vincent 63 | 64 | 0.71 Sat Aug 29 23:30:09 EDT 2009 65 | 66 | Added --local to prophet clone: list local Bonjour sources - Pedro Melo 67 | 68 | Extract out code for the filesystem replica backends so we can implement 69 | an ssh personality for them. 70 | 71 | 0.70 - 2009-08-26 72 | 73 | * Performance improvements for pull-over-HTTP 74 | * Small documentation, error message and warnings cleanups 75 | 76 | 77 | 0.69_01 - 2009-08-21 78 | 79 | * Initial release 80 | - dev release to do CPAN smoking before official release 81 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | ~$ 2 | .tmp$ 3 | .svn$ 4 | .git/* 5 | .bak$ 6 | ^blib/ 7 | pm_to_blib 8 | .gitignore 9 | ^Makefile(?:\.old)?$ 10 | .prove 11 | .shipit$ 12 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use inc::Module::Install; 4 | name('Prophet'); # App::Settings App::Settings::CLI App::WebToy App::WebToy::CLI 5 | 6 | author('clkao and jesse'); 7 | 8 | license('mit'); 9 | requires('Exporter::Lite'); 10 | requires('Params::Validate'); 11 | requires('IPC::Run3'); 12 | requires('UUID::Tiny' => '1.02'); 13 | requires('Digest::SHA'); 14 | requires('LWP::UserAgent'); # LWP::ConnCache too 15 | requires('URI'); 16 | requires('HTTP::Date'); 17 | requires( 'JSON' => '2.00' ); 18 | requires('Module::Pluggable'); 19 | requires('Proc::InvokeEditor'); 20 | requires( 'Any::Moose' => '0.04' ); 21 | requires( 'Mouse' => '0.89' ); 22 | requires('XML::Atom::SimpleFeed'); 23 | requires( 'Path::Dispatcher' => '1.02' ); 24 | requires( 'Path::Dispatcher::Declarative' => '0.03' ); 25 | requires('Time::Progress'); 26 | requires('Config::GitLike' => '1.02'); 27 | requires('MIME::Base64::URLSafe'); 28 | if ( $^O =~ /MSWin/ ) { 29 | requires( 'Win32' ); 30 | } 31 | 32 | build_requires( 'Test::Exception' => '0.26' ); 33 | 34 | use Term::ReadLine; # if we don't do this, ::Perl fails 35 | feature 'Improved interactive shell' => -default => 1, 36 | 'Term::ReadLine::Perl' => 0; 37 | feature 'Faster JSON Parsing' => -default => 1, 38 | 'JSON::XS', => '2.2222'; 39 | feature 'Web server' => -default => 1, 40 | 'File::ShareDir' => '1.00', 41 | 'HTTP::Server::Simple' => '0.40', # HTTP::Server::Simple::CGI 42 | ; 43 | feature 'HTML display' => -default => 1, 44 | 'Template::Declare' => '0.35', # Template::Declare::Tags 45 | ; 46 | feature 'Foreign replica support' => -default => 1, 47 | 'Term::ReadKey'; 48 | feature 'SQLite replica support' => -default => 1, 49 | 'DBI' => 1, 50 | 'DBD::SQLite' => 1; 51 | 52 | feature 'Maintainer testing tools' => -default => 0, 53 | 'Test::HTTP::Server::Simple', 54 | 'YAML::Syck' => 0, 55 | 'Module::Refresh' => 0, 56 | 'Test::WWW::Mechanize' => '1.16', 57 | 'Test::Pod::Coverage'; 58 | feature 'Bonjour support' => -default => 0, 59 | 'Net::Bonjour', # Net::Rendezvous::Publish 60 | ; 61 | 62 | tests('t/*.t t/*/t/*.t'); 63 | all_from('lib/Prophet.pm'); 64 | install_share 'share'; 65 | auto_install; 66 | WriteAll(); 67 | -------------------------------------------------------------------------------- /bin/dump_changesets.idx: -------------------------------------------------------------------------------- 1 | use Prophet::CLI; 2 | use Prophet::FilesystemReplica; 3 | 4 | my $cli = Prophet::CLI->new(); 5 | 6 | my $file = Prophet::Util->slurp(shift); 7 | my $fsr = Prophet::FilesystemReplica->new(app_handle => $cli->app_handle); 8 | 9 | 10 | for (1..(length($file)/Prophet::FilesystemReplica::CHG_RECORD_SIZE)) { 11 | 12 | my $result = $fsr->_changeset_index_entry(sequence_no => $_, index_file => \$file); 13 | 14 | print join("\t",@$result)."\n"; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /bin/dump_record.idx: -------------------------------------------------------------------------------- 1 | use Prophet::CLI; 2 | use Prophet::FilesystemReplica; 3 | 4 | my $cli = Prophet::CLI->new(); 5 | 6 | my $type = shift; 7 | my $uuid = shift; 8 | 9 | my @result = $cli->handle->_read_record_index(type => $type, uuid => $uuid); 10 | warn YAML::Dump(\@result); use YAML; 11 | -------------------------------------------------------------------------------- /bin/prophet: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use warnings; 3 | use strict; 4 | 5 | use Prophet::CLI; 6 | my $cli = Prophet::CLI->new(); 7 | $cli->run_one_command(@ARGV); 8 | -------------------------------------------------------------------------------- /bin/run_test_yml.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl -w 2 | use strict; 3 | use Prophet::Test::Arena; 4 | 5 | Prophet::Test::Arena->run_from_yaml; 6 | 7 | 8 | =head1 NAME 9 | 10 | run_test_yml - rerun recorded test 11 | 12 | =head1 SYNOPSIS 13 | 14 | prove -l t/generalized_sync_n_merge.t 15 | perl -Ilib bin/run_test_yml.pl RECORDED-TEST.yml 16 | 17 | =head1 DESCRIPTION 18 | 19 | You can also copy this file to a .t file, and append the yml content into the 20 | C<__DATA__> section. 21 | 22 | =cut 23 | 24 | -------------------------------------------------------------------------------- /bin/taste_recipe: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | 4 | my $script = 'bin/run_test_yml.pl'; 5 | my $file = shift; 6 | use YAML::Syck; 7 | 8 | my $d = YAML::Syck::LoadFile( $file ); 9 | 10 | use Storable 'dclone'; 11 | use Data::Dumper; 12 | use File::Temp; 13 | 14 | for (reverse 0..$#{ $d->{recipe}} ) { 15 | warn "==> removing $_ ".Dumper($d->{recipe}->[$_]); 16 | next if $d->{recipe}->[$_][1] eq 'create_record'; 17 | 18 | my $foo = dclone $d; 19 | 20 | splice(@{$foo->{recipe}}, $_, 1); 21 | 22 | my $tmpf = File::Temp->new; 23 | YAML::Syck::DumpFile($tmpf, $foo); 24 | system('perl', '-Ilib', $script, $tmpf); 25 | if ($?) { 26 | $d = $foo; 27 | } 28 | warn "result : $?"; 29 | 30 | } 31 | 32 | warn "streamlined recipe at $file.new"; 33 | 34 | YAML::Syck::DumpFile( "$file.new", $d); 35 | 36 | 37 | -------------------------------------------------------------------------------- /doc/foreign-replicas: -------------------------------------------------------------------------------- 1 | 2 | =head1 Resplutions 3 | 4 | Resolutions are stored in a seperate database because they're supposed to be propagated _like_ regular changesets but always sent before the regular changesets. 5 | 6 | =head1 Native Replicas 7 | 8 | =head2 Merge tickets 9 | 10 | =head1 Foreign Replicas 11 | 12 | A foreign replica is a (possibly read-only) data store that is not a Prophet 13 | replica (such as RT or Twitter). A Prophet replica can act as a gateway to a 14 | foreign replica. 15 | 16 | Because we can't store arbitrary metadata in foreign replicas, we do not yet 17 | support arbitrary topology sync of foreign replicas. A single Prophet-backed 18 | replica must act as a gateway to a foreign replica. It would be great if Prophet could, in a general way, allow arbitrary topology sync of foreign replicas. but it was not ever a goal. 19 | 20 | Foreign replicas never talk directly with each other. Their communciations are always intermediated by a Prophet replica. 21 | The design wasn't such that you could have multiple replicas gatewaying transactions between a pair of foreign replicas. 22 | 23 | Foreign replicas aren't really full-fledged replicas, they piggyback on another replica on the proxying host to store metadata about merges, conflicts and local id (luid) to uuid mappings. When working with Foreign Replicas, the local state handle that tracks data on behalf of a foreign database using merge tickets. Our merge tickets work like svk's. they're a high-water mark of "the most recent transaction merged from some other replica", keyed by the replica uuid of that other replica. Prophet always merges all transactions from a replica sequentially. 24 | 25 | So when bob is pushing to a foreign replica, we use metadata stored in bob's replica to interact with the foreign replica. _merge_ticket records are an example of this however, when you do a push to a foreign replica, it should be storing that transaction as merged 26 | (See App::SD::ForeignReplica::record_pushed_transaction) 27 | 28 | The test that's failing is Bob pulls a task from HM and then pushes to RT. RT never gets the HM task. 29 | 30 | the specific problem I'm seeing is when bob pushes to RT, RT needs 31 | to know what the high water mark from Hiveminder is. because RT 32 | doesn't have a full replica, it ends up accidentally using Bob's 33 | merge tickets exemplified by these two adjacent lines in the logfile: 34 | Checking metadata in BOB-UUID: (_merge_tickets, HIVEMINDER-UUID, last-changeset) -> 3 35 | RT-UUID's last_changeset_from_source(HIVEMINDER-UUID) -> 3 36 | 37 | 38 | I think state_handle should be an entirely separate replica, just as resolutions are 39 | But it should never be propagated. 40 | 41 | can't it be a replica we just don't propagate? 42 | so far, your description doesn't give me any reaason to think that ending up with an explicitly seperate state database would improve anything. and it would add more moving parts. 43 | we're being bitten by reusing the Prophet replica's records 44 | if the foreign replica had its own replica, then there would be no overlap and this issue would just go away 45 | the foreign replica is using the real replica's _merge_ticket records 46 | I _believe_ that our state handle stuff should entirely replace the need to even use those 47 | merge tickets are "most recent changeset seen from replica ABC". those are generally useful to propagate around. 48 | except in the case of the foreign replica where it only ever matters what the most recent local changest we've pushed to the foreign replica 49 | (pulling from an FR should, I believe, use regular merge tickets) 50 | 51 | 52 | =head1 Open issues 53 | 54 | Prophet::ForeignReplica should probably be subclassing the bits of code that deal with MergeTickets. 55 | 56 | also, apparently "merge tickets" is a horrible name that confuses people it may want renaming 57 | -------------------------------------------------------------------------------- /doc/jesse_test_env_setup: -------------------------------------------------------------------------------- 1 | export PATH=/usr/bin:$PATH 2 | export PERL5LIB=/Users/jesse/svk/rt-3.8/lib 3 | export JIFTY_APP_ROOT=/Users/jesse/svk/hiveminder-trunk/ 4 | export RT_DBA_USER=root 5 | export RT_DBA_PASSWORD='' 6 | -------------------------------------------------------------------------------- /doc/luid: -------------------------------------------------------------------------------- 1 | GUIDs are not great to work with. "B900A5B8-2322-11DD-A835-2B9E427B83F6" is a 2 | lot to demand of a user. And god forbid they have to actually type in that 3 | meaningless string. Substring matching doesn't help much because two GUIDs can 4 | easily differ by only a bit. 5 | 6 | Instead, we give users local IDs for each record. So instead of 7 | "B900A5B8-2322-11DD-A835-2B9E427B83F6" they might get "7". Local IDs are local 8 | to a replica - so two users can have different records with local ID "7". 9 | 10 | Because local IDs are integers, they're always distinguishable from global IDs. 11 | 12 | Local IDs are mildly fleeting. They're contained in a single file (directory?) 13 | which may be removed at any time. Every time a record is loaded, it's given 14 | a local ID which is cached so the user may use it. 15 | 16 | The local ID -> global ID mapping is contained in the $replica/local-id-cache 17 | file (directory?). 18 | 19 | -------------------------------------------------------------------------------- /doc/repository-layout: -------------------------------------------------------------------------------- 1 | SVN REPO 2 | _prophet 3 | _merge_tickets 4 | remote-source-uuid 5 | last-changeset: 1234 (changeset sequence no) 6 | $record_type 7 | uuid-1234 8 | this file's svn props are the record properties 9 | uuid-1235 10 | uuid-1236 11 | -------------------------------------------------------------------------------- /doc/tab-completion: -------------------------------------------------------------------------------- 1 | zsh: 2 | sudo ln -vis `pwd`/etc/prophet.zsh /usr/share/zsh/site-functions/_prophet 3 | (tested on Mac OS X 10.6) 4 | 5 | bash: 6 | symlink etc/prophet.bash to your bash_completion.d directory, if you're using macports this 7 | will be /opt/local/etc/bash_completion.d 8 | On a linux platform, this is likely to be /etc/bash_completion.d 9 | -------------------------------------------------------------------------------- /doc/todo: -------------------------------------------------------------------------------- 1 | Todo 2 | 3 | - document sd & replica format 4 | 5 | 6 | - native replica type isn't properly transactional. aborting at the wrong time will cause great sadness and possible corruption 7 | 8 | 9 | - make merge aware of database uuids 10 | - and does it mean if the db is initialized with a pull, it uses the same UUID 11 | - yes. in general, dbs should be initialized with pull or be new projects 12 | - merging between replicas with different uuids should require a 'force' argument of some kind. 13 | 14 | "publish my changes for remote pulling" - mostly done. needs test and cleanup, "publish" scp wrapper 15 | 16 | - move merge-ticket logic out of handle and only provides metadata storage 17 | - validation on bug tracker fields - severity 18 | - Replace this todo list with a svb database 19 | - elegant support for large attachments 20 | - RESTy web server API to let third-parties build non-perl apps against a Prophet Depot 21 | - define a value for a property that is a reference to: 22 | - another record 23 | - a set of records @done 24 | 25 | - sketch out RT scrips replacement 26 | 27 | 28 | 29 | 30 | 31 | Saturday done 32 | - implement a simple Prophet::Replica::Hiveminder for "personal tasks only" 33 | - extract the reusable bits of Prophet::Replica::RT to 34 | Prophet::ForeignReplica 35 | - implement uuids for prophet databases DONE 36 | - light dinner 37 | - dinner @done 38 | 39 | 40 | 41 | 42 | 43 | 44 | Todo after saturday: 45 | 46 | 47 | 48 | Archive: 49 | 50 | - ability to add comments to a bug (visible history entries) 51 | - maybe long-prop edits 52 | 53 | 54 | 55 | - when committing any change: 56 | - record the original depot uuid and change sequence_no as revprops 57 | - record two merge tickets: 58 | - sequence_no from the source that gave it to us 59 | - sequence_no from the original recording source 60 | 61 | - naive support for large attachments 62 | - ability to pull non-conflicting updates from a remote db 63 | - implement merge of conflicts with: "local always wins" 64 | - record conflict resolution data 65 | - reuse conflict resolution data on repeated resolve 66 | - ability to 'pull' conflicting updates from a remote db 67 | - prompt for resolution of conflicts 68 | - handle file_conflict 69 | 70 | - test byzantine sync behaviour 71 | - handle conflicting conflict resolutions 72 | - base bug tracking schema 73 | 74 | - ::CLI should automatically discover an app's model class based on the type name @done 75 | - Creation of bug tracking model classes @done 76 | - status @done 77 | - relations between models @done 78 | - find out what the remote _would_ pull by inspecting its merge tickets on @done 79 | - current replica @done 80 | - once we do that, we can find out who _we_ have synced from after that point, right? Then we want: @done 81 | - anyone we have a merge ticket for _since_ the last time the other party saw us. @done 82 | - nobu @done 83 | - get RT to give us a list of ticket records matching our query @done 84 | - get rt ro give us a list of history entries on those ticket records @done 85 | -------------------------------------------------------------------------------- /doc/web_form_handling: -------------------------------------------------------------------------------- 1 | # in the dispatcher: 2 | 3 | # get all form fields that match the spec 4 | # bundle them by record 5 | # order them by the desired order 6 | # canonicalize 7 | # validate 8 | # execute if we're to execute 9 | # on failure 10 | # rerender the current page 11 | # on success 12 | # go to "next page" 13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/prophet.bash: -------------------------------------------------------------------------------- 1 | function _prophet_() 2 | { 3 | COMPREPLY=($($1 _gencomp ${COMP_WORDS[COMP_CWORD]})) 4 | } 5 | 6 | complete -F _prophet_ myprophetapp 7 | -------------------------------------------------------------------------------- /etc/prophet.zsh: -------------------------------------------------------------------------------- 1 | #compdef prophet sd 2 | 3 | typeset -a prophet_completions 4 | prophet_completions=($($words[1] _gencomp $words[2,-1])) 5 | compadd $prophet_completions 6 | 7 | -------------------------------------------------------------------------------- /inc/Module/Install/AutoInstall.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::AutoInstall; 3 | 4 | use strict; 5 | use Module::Install::Base (); 6 | 7 | use vars qw{$VERSION @ISA $ISCORE}; 8 | BEGIN { 9 | $VERSION = '1.00'; 10 | @ISA = 'Module::Install::Base'; 11 | $ISCORE = 1; 12 | } 13 | 14 | sub AutoInstall { $_[0] } 15 | 16 | sub run { 17 | my $self = shift; 18 | $self->auto_install_now(@_); 19 | } 20 | 21 | sub write { 22 | my $self = shift; 23 | $self->auto_install(@_); 24 | } 25 | 26 | sub auto_install { 27 | my $self = shift; 28 | return if $self->{done}++; 29 | 30 | # Flatten array of arrays into a single array 31 | my @core = map @$_, map @$_, grep ref, 32 | $self->build_requires, $self->requires; 33 | 34 | my @config = @_; 35 | 36 | # We'll need Module::AutoInstall 37 | $self->include('Module::AutoInstall'); 38 | require Module::AutoInstall; 39 | 40 | my @features_require = Module::AutoInstall->import( 41 | (@config ? (-config => \@config) : ()), 42 | (@core ? (-core => \@core) : ()), 43 | $self->features, 44 | ); 45 | 46 | my %seen; 47 | my @requires = map @$_, map @$_, grep ref, $self->requires; 48 | while (my ($mod, $ver) = splice(@requires, 0, 2)) { 49 | $seen{$mod}{$ver}++; 50 | } 51 | my @build_requires = map @$_, map @$_, grep ref, $self->build_requires; 52 | while (my ($mod, $ver) = splice(@build_requires, 0, 2)) { 53 | $seen{$mod}{$ver}++; 54 | } 55 | my @configure_requires = map @$_, map @$_, grep ref, $self->configure_requires; 56 | while (my ($mod, $ver) = splice(@configure_requires, 0, 2)) { 57 | $seen{$mod}{$ver}++; 58 | } 59 | 60 | my @deduped; 61 | while (my ($mod, $ver) = splice(@features_require, 0, 2)) { 62 | push @deduped, $mod => $ver unless $seen{$mod}{$ver}++; 63 | } 64 | 65 | $self->requires(@deduped); 66 | 67 | $self->makemaker_args( Module::AutoInstall::_make_args() ); 68 | 69 | my $class = ref($self); 70 | $self->postamble( 71 | "# --- $class section:\n" . 72 | Module::AutoInstall::postamble() 73 | ); 74 | } 75 | 76 | sub auto_install_now { 77 | my $self = shift; 78 | $self->auto_install(@_); 79 | Module::AutoInstall::do_install(); 80 | } 81 | 82 | 1; 83 | -------------------------------------------------------------------------------- /inc/Module/Install/Base.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::Base; 3 | 4 | use strict 'vars'; 5 | use vars qw{$VERSION}; 6 | BEGIN { 7 | $VERSION = '1.00'; 8 | } 9 | 10 | # Suspend handler for "redefined" warnings 11 | BEGIN { 12 | my $w = $SIG{__WARN__}; 13 | $SIG{__WARN__} = sub { $w }; 14 | } 15 | 16 | #line 42 17 | 18 | sub new { 19 | my $class = shift; 20 | unless ( defined &{"${class}::call"} ) { 21 | *{"${class}::call"} = sub { shift->_top->call(@_) }; 22 | } 23 | unless ( defined &{"${class}::load"} ) { 24 | *{"${class}::load"} = sub { shift->_top->load(@_) }; 25 | } 26 | bless { @_ }, $class; 27 | } 28 | 29 | #line 61 30 | 31 | sub AUTOLOAD { 32 | local $@; 33 | my $func = eval { shift->_top->autoload } or return; 34 | goto &$func; 35 | } 36 | 37 | #line 75 38 | 39 | sub _top { 40 | $_[0]->{_top}; 41 | } 42 | 43 | #line 90 44 | 45 | sub admin { 46 | $_[0]->_top->{admin} 47 | or 48 | Module::Install::Base::FakeAdmin->new; 49 | } 50 | 51 | #line 106 52 | 53 | sub is_admin { 54 | ! $_[0]->admin->isa('Module::Install::Base::FakeAdmin'); 55 | } 56 | 57 | sub DESTROY {} 58 | 59 | package Module::Install::Base::FakeAdmin; 60 | 61 | use vars qw{$VERSION}; 62 | BEGIN { 63 | $VERSION = $Module::Install::Base::VERSION; 64 | } 65 | 66 | my $fake; 67 | 68 | sub new { 69 | $fake ||= bless(\@_, $_[0]); 70 | } 71 | 72 | sub AUTOLOAD {} 73 | 74 | sub DESTROY {} 75 | 76 | # Restore warning handler 77 | BEGIN { 78 | $SIG{__WARN__} = $SIG{__WARN__}->(); 79 | } 80 | 81 | 1; 82 | 83 | #line 159 84 | -------------------------------------------------------------------------------- /inc/Module/Install/Can.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::Can; 3 | 4 | use strict; 5 | use Config (); 6 | use File::Spec (); 7 | use ExtUtils::MakeMaker (); 8 | use Module::Install::Base (); 9 | 10 | use vars qw{$VERSION @ISA $ISCORE}; 11 | BEGIN { 12 | $VERSION = '1.00'; 13 | @ISA = 'Module::Install::Base'; 14 | $ISCORE = 1; 15 | } 16 | 17 | # check if we can load some module 18 | ### Upgrade this to not have to load the module if possible 19 | sub can_use { 20 | my ($self, $mod, $ver) = @_; 21 | $mod =~ s{::|\\}{/}g; 22 | $mod .= '.pm' unless $mod =~ /\.pm$/i; 23 | 24 | my $pkg = $mod; 25 | $pkg =~ s{/}{::}g; 26 | $pkg =~ s{\.pm$}{}i; 27 | 28 | local $@; 29 | eval { require $mod; $pkg->VERSION($ver || 0); 1 }; 30 | } 31 | 32 | # check if we can run some command 33 | sub can_run { 34 | my ($self, $cmd) = @_; 35 | 36 | my $_cmd = $cmd; 37 | return $_cmd if (-x $_cmd or $_cmd = MM->maybe_command($_cmd)); 38 | 39 | for my $dir ((split /$Config::Config{path_sep}/, $ENV{PATH}), '.') { 40 | next if $dir eq ''; 41 | my $abs = File::Spec->catfile($dir, $_[1]); 42 | return $abs if (-x $abs or $abs = MM->maybe_command($abs)); 43 | } 44 | 45 | return; 46 | } 47 | 48 | # can we locate a (the) C compiler 49 | sub can_cc { 50 | my $self = shift; 51 | my @chunks = split(/ /, $Config::Config{cc}) or return; 52 | 53 | # $Config{cc} may contain args; try to find out the program part 54 | while (@chunks) { 55 | return $self->can_run("@chunks") || (pop(@chunks), next); 56 | } 57 | 58 | return; 59 | } 60 | 61 | # Fix Cygwin bug on maybe_command(); 62 | if ( $^O eq 'cygwin' ) { 63 | require ExtUtils::MM_Cygwin; 64 | require ExtUtils::MM_Win32; 65 | if ( ! defined(&ExtUtils::MM_Cygwin::maybe_command) ) { 66 | *ExtUtils::MM_Cygwin::maybe_command = sub { 67 | my ($self, $file) = @_; 68 | if ($file =~ m{^/cygdrive/}i and ExtUtils::MM_Win32->can('maybe_command')) { 69 | ExtUtils::MM_Win32->maybe_command($file); 70 | } else { 71 | ExtUtils::MM_Unix->maybe_command($file); 72 | } 73 | } 74 | } 75 | } 76 | 77 | 1; 78 | 79 | __END__ 80 | 81 | #line 156 82 | -------------------------------------------------------------------------------- /inc/Module/Install/Fetch.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::Fetch; 3 | 4 | use strict; 5 | use Module::Install::Base (); 6 | 7 | use vars qw{$VERSION @ISA $ISCORE}; 8 | BEGIN { 9 | $VERSION = '1.00'; 10 | @ISA = 'Module::Install::Base'; 11 | $ISCORE = 1; 12 | } 13 | 14 | sub get_file { 15 | my ($self, %args) = @_; 16 | my ($scheme, $host, $path, $file) = 17 | $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return; 18 | 19 | if ( $scheme eq 'http' and ! eval { require LWP::Simple; 1 } ) { 20 | $args{url} = $args{ftp_url} 21 | or (warn("LWP support unavailable!\n"), return); 22 | ($scheme, $host, $path, $file) = 23 | $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return; 24 | } 25 | 26 | $|++; 27 | print "Fetching '$file' from $host... "; 28 | 29 | unless (eval { require Socket; Socket::inet_aton($host) }) { 30 | warn "'$host' resolve failed!\n"; 31 | return; 32 | } 33 | 34 | return unless $scheme eq 'ftp' or $scheme eq 'http'; 35 | 36 | require Cwd; 37 | my $dir = Cwd::getcwd(); 38 | chdir $args{local_dir} or return if exists $args{local_dir}; 39 | 40 | if (eval { require LWP::Simple; 1 }) { 41 | LWP::Simple::mirror($args{url}, $file); 42 | } 43 | elsif (eval { require Net::FTP; 1 }) { eval { 44 | # use Net::FTP to get past firewall 45 | my $ftp = Net::FTP->new($host, Passive => 1, Timeout => 600); 46 | $ftp->login("anonymous", 'anonymous@example.com'); 47 | $ftp->cwd($path); 48 | $ftp->binary; 49 | $ftp->get($file) or (warn("$!\n"), return); 50 | $ftp->quit; 51 | } } 52 | elsif (my $ftp = $self->can_run('ftp')) { eval { 53 | # no Net::FTP, fallback to ftp.exe 54 | require FileHandle; 55 | my $fh = FileHandle->new; 56 | 57 | local $SIG{CHLD} = 'IGNORE'; 58 | unless ($fh->open("|$ftp -n")) { 59 | warn "Couldn't open ftp: $!\n"; 60 | chdir $dir; return; 61 | } 62 | 63 | my @dialog = split(/\n/, <<"END_FTP"); 64 | open $host 65 | user anonymous anonymous\@example.com 66 | cd $path 67 | binary 68 | get $file $file 69 | quit 70 | END_FTP 71 | foreach (@dialog) { $fh->print("$_\n") } 72 | $fh->close; 73 | } } 74 | else { 75 | warn "No working 'ftp' program available!\n"; 76 | chdir $dir; return; 77 | } 78 | 79 | unless (-f $file) { 80 | warn "Fetching failed: $@\n"; 81 | chdir $dir; return; 82 | } 83 | 84 | return if exists $args{size} and -s $file != $args{size}; 85 | system($args{run}) if exists $args{run}; 86 | unlink($file) if $args{remove}; 87 | 88 | print(((!exists $args{check_for} or -e $args{check_for}) 89 | ? "done!" : "failed! ($!)"), "\n"); 90 | chdir $dir; return !$?; 91 | } 92 | 93 | 1; 94 | -------------------------------------------------------------------------------- /inc/Module/Install/Include.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::Include; 3 | 4 | use strict; 5 | use Module::Install::Base (); 6 | 7 | use vars qw{$VERSION @ISA $ISCORE}; 8 | BEGIN { 9 | $VERSION = '1.00'; 10 | @ISA = 'Module::Install::Base'; 11 | $ISCORE = 1; 12 | } 13 | 14 | sub include { 15 | shift()->admin->include(@_); 16 | } 17 | 18 | sub include_deps { 19 | shift()->admin->include_deps(@_); 20 | } 21 | 22 | sub auto_include { 23 | shift()->admin->auto_include(@_); 24 | } 25 | 26 | sub auto_include_deps { 27 | shift()->admin->auto_include_deps(@_); 28 | } 29 | 30 | sub auto_include_dependent_dists { 31 | shift()->admin->auto_include_dependent_dists(@_); 32 | } 33 | 34 | 1; 35 | -------------------------------------------------------------------------------- /inc/Module/Install/Share.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::Share; 3 | 4 | use strict; 5 | use Module::Install::Base (); 6 | use File::Find (); 7 | use ExtUtils::Manifest (); 8 | 9 | use vars qw{$VERSION @ISA $ISCORE}; 10 | BEGIN { 11 | $VERSION = '1.00'; 12 | @ISA = 'Module::Install::Base'; 13 | $ISCORE = 1; 14 | } 15 | 16 | sub install_share { 17 | my $self = shift; 18 | my $dir = @_ ? pop : 'share'; 19 | my $type = @_ ? shift : 'dist'; 20 | unless ( defined $type and $type eq 'module' or $type eq 'dist' ) { 21 | die "Illegal or invalid share dir type '$type'"; 22 | } 23 | unless ( defined $dir and -d $dir ) { 24 | require Carp; 25 | Carp::croak("Illegal or missing directory install_share param"); 26 | } 27 | 28 | # Split by type 29 | my $S = ($^O eq 'MSWin32') ? "\\" : "\/"; 30 | 31 | my $root; 32 | if ( $type eq 'dist' ) { 33 | die "Too many parameters to install_share" if @_; 34 | 35 | # Set up the install 36 | $root = "\$(INST_LIB)${S}auto${S}share${S}dist${S}\$(DISTNAME)"; 37 | } else { 38 | my $module = Module::Install::_CLASS($_[0]); 39 | unless ( defined $module ) { 40 | die "Missing or invalid module name '$_[0]'"; 41 | } 42 | $module =~ s/::/-/g; 43 | 44 | $root = "\$(INST_LIB)${S}auto${S}share${S}module${S}$module"; 45 | } 46 | 47 | my $manifest = -r 'MANIFEST' ? ExtUtils::Manifest::maniread() : undef; 48 | my $skip_checker = $ExtUtils::Manifest::VERSION >= 1.54 49 | ? ExtUtils::Manifest::maniskip() 50 | : ExtUtils::Manifest::_maniskip(); 51 | my $postamble = ''; 52 | my $perm_dir = eval($ExtUtils::MakeMaker::VERSION) >= 6.52 ? '$(PERM_DIR)' : 755; 53 | File::Find::find({ 54 | no_chdir => 1, 55 | wanted => sub { 56 | my $path = File::Spec->abs2rel($_, $dir); 57 | if (-d $_) { 58 | return if $skip_checker->($File::Find::name); 59 | $postamble .=<<"END"; 60 | \t\$(NOECHO) \$(MKPATH) "$root${S}$path" 61 | \t\$(NOECHO) \$(CHMOD) $perm_dir "$root${S}$path" 62 | END 63 | } 64 | else { 65 | return if ref $manifest 66 | && !exists $manifest->{$File::Find::name}; 67 | return if $skip_checker->($File::Find::name); 68 | $postamble .=<<"END"; 69 | \t\$(NOECHO) \$(CP) "$dir${S}$path" "$root${S}$path" 70 | END 71 | } 72 | }, 73 | }, $dir); 74 | 75 | # Set up the install 76 | $self->postamble(<<"END_MAKEFILE"); 77 | config :: 78 | $postamble 79 | 80 | END_MAKEFILE 81 | 82 | # The above appears to behave incorrectly when used with old versions 83 | # of ExtUtils::Install (known-bad on RHEL 3, with 5.8.0) 84 | # So when we need to install a share directory, make sure we add a 85 | # dependency on a moderately new version of ExtUtils::MakeMaker. 86 | $self->build_requires( 'ExtUtils::MakeMaker' => '6.11' ); 87 | 88 | # 99% of the time we don't want to index a shared dir 89 | $self->no_index( directory => $dir ); 90 | } 91 | 92 | 1; 93 | 94 | __END__ 95 | 96 | #line 154 97 | -------------------------------------------------------------------------------- /inc/Module/Install/Win32.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::Win32; 3 | 4 | use strict; 5 | use Module::Install::Base (); 6 | 7 | use vars qw{$VERSION @ISA $ISCORE}; 8 | BEGIN { 9 | $VERSION = '1.00'; 10 | @ISA = 'Module::Install::Base'; 11 | $ISCORE = 1; 12 | } 13 | 14 | # determine if the user needs nmake, and download it if needed 15 | sub check_nmake { 16 | my $self = shift; 17 | $self->load('can_run'); 18 | $self->load('get_file'); 19 | 20 | require Config; 21 | return unless ( 22 | $^O eq 'MSWin32' and 23 | $Config::Config{make} and 24 | $Config::Config{make} =~ /^nmake\b/i and 25 | ! $self->can_run('nmake') 26 | ); 27 | 28 | print "The required 'nmake' executable not found, fetching it...\n"; 29 | 30 | require File::Basename; 31 | my $rv = $self->get_file( 32 | url => 'http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe', 33 | ftp_url => 'ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe', 34 | local_dir => File::Basename::dirname($^X), 35 | size => 51928, 36 | run => 'Nmake15.exe /o > nul', 37 | check_for => 'Nmake.exe', 38 | remove => 1, 39 | ); 40 | 41 | die <<'END_MESSAGE' unless $rv; 42 | 43 | ------------------------------------------------------------------------------- 44 | 45 | Since you are using Microsoft Windows, you will need the 'nmake' utility 46 | before installation. It's available at: 47 | 48 | http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe 49 | or 50 | ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe 51 | 52 | Please download the file manually, save it to a directory in %PATH% (e.g. 53 | C:\WINDOWS\COMMAND\), then launch the MS-DOS command line shell, "cd" to 54 | that directory, and run "Nmake15.exe" from there; that will create the 55 | 'nmake.exe' file needed by this module. 56 | 57 | You may then resume the installation process described in README. 58 | 59 | ------------------------------------------------------------------------------- 60 | END_MESSAGE 61 | 62 | } 63 | 64 | 1; 65 | -------------------------------------------------------------------------------- /inc/Module/Install/WriteAll.pm: -------------------------------------------------------------------------------- 1 | #line 1 2 | package Module::Install::WriteAll; 3 | 4 | use strict; 5 | use Module::Install::Base (); 6 | 7 | use vars qw{$VERSION @ISA $ISCORE}; 8 | BEGIN { 9 | $VERSION = '1.00'; 10 | @ISA = qw{Module::Install::Base}; 11 | $ISCORE = 1; 12 | } 13 | 14 | sub WriteAll { 15 | my $self = shift; 16 | my %args = ( 17 | meta => 1, 18 | sign => 0, 19 | inline => 0, 20 | check_nmake => 1, 21 | @_, 22 | ); 23 | 24 | $self->sign(1) if $args{sign}; 25 | $self->admin->WriteAll(%args) if $self->is_admin; 26 | 27 | $self->check_nmake if $args{check_nmake}; 28 | unless ( $self->makemaker_args->{PL_FILES} ) { 29 | # XXX: This still may be a bit over-defensive... 30 | unless ($self->makemaker(6.25)) { 31 | $self->makemaker_args( PL_FILES => {} ) if -f 'Build.PL'; 32 | } 33 | } 34 | 35 | # Until ExtUtils::MakeMaker support MYMETA.yml, make sure 36 | # we clean it up properly ourself. 37 | $self->realclean_files('MYMETA.yml'); 38 | 39 | if ( $args{inline} ) { 40 | $self->Inline->write; 41 | } else { 42 | $self->Makefile->write; 43 | } 44 | 45 | # The Makefile write process adds a couple of dependencies, 46 | # so write the META.yml files after the Makefile. 47 | if ( $args{meta} ) { 48 | $self->Meta->write; 49 | } 50 | 51 | # Experimental support for MYMETA 52 | if ( $ENV{X_MYMETA} ) { 53 | if ( $ENV{X_MYMETA} eq 'JSON' ) { 54 | $self->Meta->write_mymeta_json; 55 | } else { 56 | $self->Meta->write_mymeta_yaml; 57 | } 58 | } 59 | 60 | return 1; 61 | } 62 | 63 | 1; 64 | -------------------------------------------------------------------------------- /lib/Prophet.pm: -------------------------------------------------------------------------------- 1 | use warnings; 2 | use strict; 3 | 4 | package Prophet; 5 | 6 | our $VERSION = '0.743'; 7 | 8 | =head1 NAME 9 | 10 | Prophet 11 | 12 | =head1 DESCRIPTION 13 | 14 | Prophet is a distributed database system designed for small to medium 15 | scale social database applications. Our early targets include things 16 | such as bug tracking. 17 | 18 | 19 | =head2 Design goals 20 | 21 | =over 22 | 23 | =item Arbitrary record schema 24 | 25 | =item Replication 26 | 27 | =item Disconnected operation 28 | 29 | =item Peer to peer synchronization 30 | 31 | =back 32 | 33 | 34 | 35 | =head2 Design constraints 36 | 37 | =over 38 | 39 | =item Scaling 40 | 41 | We don't currently intend for the first implementation of Prophet to 42 | scale to databases with millions of rows or hundreds of concurrent 43 | users. There's nothing that makes the design infeasible, but the 44 | infrastructure necessary for such a system will...needlessly hamstring it. 45 | 46 | =back 47 | 48 | =cut 49 | 50 | 1; 51 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/CollectionCommand.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::CollectionCommand; 2 | use Any::Moose 'Role'; 3 | with 'Prophet::CLI::RecordCommand'; 4 | 5 | use Params::Validate; 6 | 7 | sub get_collection_object { 8 | my $self = shift; 9 | my %args = validate(@_, { 10 | type => { default => $self->type }, 11 | }); 12 | 13 | my $class = $self->_get_record_object(type => $args{type})->collection_class; 14 | Prophet::App->require($class); 15 | 16 | my $records = $class->new( 17 | app_handle => $self->app_handle, 18 | handle => $self->handle, 19 | type => $args{type} || $self->type, 20 | ); 21 | 22 | return $records; 23 | } 24 | 25 | no Any::Moose 'Role'; 26 | 27 | 1; 28 | 29 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Create.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Create; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | with 'Prophet::CLI::RecordCommand'; 5 | has '+uuid' => ( required => 0); 6 | 7 | has record => ( 8 | is => 'rw', 9 | isa => 'Prophet::Record', 10 | documentation => 'The record object of the created record.', 11 | ); 12 | 13 | sub usage_msg { 14 | my $self = shift; 15 | my ($cmd, $type_and_subcmd) = $self->get_cmd_and_subcmd_names; 16 | 17 | return <<"END_USAGE"; 18 | usage: ${cmd}${type_and_subcmd} -- prop1=foo prop2=bar 19 | END_USAGE 20 | } 21 | 22 | sub run { 23 | my $self = shift; 24 | 25 | $self->print_usage if $self->has_arg('h'); 26 | 27 | my $record = $self->_get_record_object; 28 | my ($val, $msg) = $record->create( props => $self->edit_props ); 29 | if (!$val) { 30 | warn "Unable to create record: " . $msg . "\n"; 31 | } 32 | if (!$record->uuid) { 33 | warn "Failed to create " . $record->record_type . "\n"; 34 | return; 35 | } 36 | 37 | $self->record($record); 38 | 39 | print "Created " . $record->record_type . " " . $record->luid . " (".$record->uuid.")"."\n"; 40 | } 41 | 42 | __PACKAGE__->meta->make_immutable; 43 | no Any::Moose; 44 | 45 | 1; 46 | 47 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Delete.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Delete; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | with 'Prophet::CLI::RecordCommand'; 5 | 6 | sub usage_msg { 7 | my $self = shift; 8 | my ($cmd, $type_and_subcmd) = $self->get_cmd_and_subcmd_names; 9 | 10 | return <<"END_USAGE"; 11 | usage: ${cmd}${type_and_subcmd} 12 | END_USAGE 13 | } 14 | 15 | sub run { 16 | my $self = shift; 17 | 18 | $self->print_usage if $self->has_arg('h'); 19 | 20 | $self->require_uuid; 21 | my $record = $self->_load_record; 22 | 23 | if ( $record->delete ) { 24 | print $record->type . " " . $record->uuid . " deleted.\n"; 25 | } else { 26 | print $record->type . " " . $record->uuid . "could not be deleted.\n"; 27 | } 28 | 29 | } 30 | 31 | __PACKAGE__->meta->make_immutable; 32 | no Any::Moose; 33 | 34 | 1; 35 | 36 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Export.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Export; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | 5 | sub usage_msg { 6 | my $self = shift; 7 | my $cmd = $self->cli->get_script_name; 8 | 9 | return <<"END_USAGE"; 10 | usage: ${cmd}export --path [--format feed] 11 | END_USAGE 12 | } 13 | 14 | sub run { 15 | my $self = shift; 16 | my $class; 17 | 18 | $self->print_usage if $self->has_arg('h'); 19 | 20 | unless ($self->context->has_arg('path')) { 21 | warn "No --path argument specified!\n"; 22 | $self->print_usage; 23 | } 24 | 25 | if ($self->context->has_arg('format') && ($self->context->arg('format') eq 'feed') ){ 26 | $class = 'Prophet::ReplicaFeedExporter'; 27 | } 28 | else { 29 | $class = 'Prophet::ReplicaExporter'; 30 | } 31 | 32 | $self->app_handle->require ($class); 33 | my $exporter = $class->new( 34 | { target_path => $self->context->arg('path'), 35 | source_replica => $self->app_handle->handle, 36 | app_handle => $self->app_handle 37 | } 38 | ); 39 | 40 | $exporter->export(); 41 | } 42 | 43 | __PACKAGE__->meta->make_immutable; 44 | no Any::Moose; 45 | 46 | 1; 47 | 48 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/History.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::History; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | with 'Prophet::CLI::RecordCommand'; 5 | 6 | sub usage_msg { 7 | my $self = shift; 8 | my ($cmd, $type_and_subcmd) = $self->get_cmd_and_subcmd_names; 9 | 10 | return <<"END_USAGE"; 11 | usage: ${cmd}${type_and_subcmd} 12 | END_USAGE 13 | } 14 | 15 | sub run { 16 | my $self = shift; 17 | 18 | $self->print_usage if $self->has_arg('h'); 19 | 20 | $self->require_uuid; 21 | my $record = $self->_load_record; 22 | 23 | print $record->history_as_string; 24 | } 25 | 26 | __PACKAGE__->meta->make_immutable; 27 | no Any::Moose; 28 | 29 | 1; 30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Info.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Info; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | 5 | sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), l => 'local' }; 6 | 7 | sub usage_msg { 8 | my $self = shift; 9 | my $cmd = $self->cli->get_script_name; 10 | 11 | return <<"END_USAGE"; 12 | usage: ${cmd}info 13 | END_USAGE 14 | } 15 | 16 | sub run { 17 | my $self = shift; 18 | 19 | $self->print_usage if $self->has_arg('h'); 20 | 21 | print "Records Database\n"; 22 | print "----------------\n"; 23 | 24 | print "Location: ".$self->handle->url." (@{[ref($self->handle)]})\n"; 25 | print "Database UUID: ".$self->handle->db_uuid."\n"; 26 | print "Replica UUID: ".$self->handle->uuid."\n"; 27 | print "Changesets: ".$self->handle->latest_sequence_no."\n"; 28 | print "Known types: ".join(',', @{$self->handle->list_types} )."\n\n"; 29 | 30 | print "Resolutions Database\n"; 31 | print "--------------------\n"; 32 | 33 | print "Location: " 34 | .$self->handle->resolution_db_handle->url." (@{[ref($self->handle)]})\n"; 35 | print "Database UUID: " 36 | .$self->handle->resolution_db_handle->db_uuid."\n"; 37 | print "Replica UUID: " 38 | .$self->handle->resolution_db_handle->uuid."\n"; 39 | print "Changesets: " 40 | .$self->handle->resolution_db_handle->latest_sequence_no."\n"; 41 | # known types get very unwieldy for resolutions 42 | # print "Known types: " 43 | # .join(',', @{$self->handle->resolution_db_handle->list_types} )."\n"; 44 | } 45 | 46 | __PACKAGE__->meta->make_immutable; 47 | no Any::Moose; 48 | 49 | 1; 50 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Init.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Init; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | 5 | sub usage_msg { 6 | my $self = shift; 7 | my $cmd = $self->cli->get_script_name; 8 | 9 | return <<"END_USAGE"; 10 | usage: ${cmd}init 11 | END_USAGE 12 | } 13 | 14 | sub run { 15 | my $self = shift; 16 | 17 | $self->print_usage if $self->has_arg('h'); 18 | 19 | if ($self->app_handle->handle->replica_exists) { 20 | die "Your Prophet database already exists.\n"; 21 | } 22 | 23 | $self->app_handle->handle->after_initialize( sub { shift->app_handle->set_db_defaults } ); 24 | $self->app_handle->handle->initialize; 25 | print "Initialized your new Prophet database.\n"; 26 | 27 | # create new config section for this replica 28 | my $url = $self->app_handle->handle->url; 29 | $self->app_handle->config->set( 30 | key => 'replica.'.$url.'.uuid', 31 | value => $self->app_handle->handle->uuid, 32 | filename => $self->app_handle->config->replica_config_file, 33 | ); 34 | } 35 | 36 | 37 | __PACKAGE__->meta->make_immutable; 38 | no Any::Moose; 39 | 40 | 1; 41 | 42 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Mirror.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Mirror; 2 | use Any::Moose; 3 | use Params::Validate qw/:all/; 4 | 5 | extends 'Prophet::CLI::Command'; 6 | with 'Prophet::CLI::MirrorCommand'; 7 | 8 | has source => ( isa => 'Prophet::Replica', is => 'rw'); 9 | has target => ( isa => 'Prophet::Replica', is => 'rw'); 10 | 11 | sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), f => 'force' }; 12 | 13 | sub usage_msg { 14 | my $self = shift; 15 | my $cmd = $self->cli->get_script_name; 16 | 17 | return <<"END_USAGE"; 18 | usage: ${cmd}mirror --from 19 | END_USAGE 20 | } 21 | 22 | sub run { 23 | my $self = shift; 24 | Prophet::CLI->end_pager(); 25 | 26 | $self->print_usage if $self->has_arg('h'); 27 | 28 | $self->validate_args(); 29 | 30 | my $source = Prophet::Replica->get_handle( url => $self->arg('from'), app_handle => $self->app_handle,); 31 | unless ( $source->replica_exists ) { 32 | print "The source replica '@{[$source->url]}' doesn't exist or is unreadable."; 33 | exit 1; 34 | } 35 | 36 | my $target = $self->get_cache_for_source($source); 37 | $self->sync_cache_from_source( target=> $target, source => $source); 38 | print "\nDone.\n"; 39 | } 40 | 41 | 42 | sub validate_args { 43 | my $self = shift; 44 | unless ( $self->has_arg('from') ) { 45 | warn "No --from specified!\n"; 46 | $self->print_usage; 47 | } 48 | } 49 | 50 | __PACKAGE__->meta->make_immutable; 51 | no Any::Moose; 52 | 53 | 1; 54 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Push.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Push; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command::Merge'; 4 | 5 | sub usage_msg { 6 | my $self = shift; 7 | my $cmd = $self->cli->get_script_name; 8 | 9 | return <<"END_USAGE"; 10 | usage: ${cmd}push --to [--force] 11 | END_USAGE 12 | } 13 | 14 | sub run { 15 | my $self = shift; 16 | 17 | Prophet::CLI->end_pager(); 18 | 19 | $self->print_usage if $self->has_arg('h'); 20 | 21 | $self->validate_args; 22 | 23 | # sub out friendly names for replica URLs if possible 24 | my %previous_sources_by_name_push_url 25 | = $self->app_handle->config->sources( variable => 'push-url' ); 26 | my %previous_sources_by_name_url = $self->app_handle->config->sources; 27 | 28 | my $original_to = $self->arg('to'); 29 | $self->set_arg( 'to' => exists $previous_sources_by_name_push_url{$self->arg('to')} 30 | ? $previous_sources_by_name_push_url{$self->arg('to')} 31 | : exists $previous_sources_by_name_url{$self->arg('to')} 32 | ? $previous_sources_by_name_url{$self->arg('to')} 33 | : $self->arg('to') 34 | ); 35 | 36 | # don't let users push to foreign replicas they haven't pulled from yet 37 | # without --force 38 | my %seen_replicas_by_url = $self->config->sources( by_variable => 1 ); 39 | my %seen_replicas_by_pull_url = $self->config->sources( 40 | by_variable => 1, 41 | variable => 'pull-url', 42 | ); 43 | 44 | (my $class, undef, undef) = Prophet::Replica->_url_to_replica_class( 45 | url => $self->arg('to'), 46 | app_handle => $self->app_handle, 47 | ); 48 | 49 | die "No replica found at '".$self->arg('to')."'.\n" unless $class; 50 | 51 | die "Can't push to HTTP replicas! You probably want to publish instead.\n" 52 | if $class->isa("Prophet::Replica::http"); 53 | 54 | die "Can't push to foreign replica that's never been pulled from! (Override with --force.)\n" 55 | unless 56 | $class->isa('Prophet::ForeignReplica') && 57 | ( $self->has_arg('force') || 58 | ( exists $seen_replicas_by_url{$self->arg('to')} || 59 | exists $seen_replicas_by_pull_url{$self->arg('to')} )); 60 | 61 | # prepare to run merge command (superclass) 62 | $self->set_arg( from => $self->handle->url ); 63 | $self->set_arg( db_uuid => $self->handle->db_uuid ); 64 | 65 | $self->SUPER::run(); 66 | 67 | # we want to record only the replica we're pushing TO, and only if we 68 | # weren't using a friendly name already 69 | $self->record_replica_in_config($self->arg('to'), $self->target->uuid) 70 | if $self->arg('to') eq $original_to; 71 | } 72 | 73 | sub validate_args { 74 | my $self = shift; 75 | 76 | unless ( $self->context->has_arg('to') ) { 77 | warn "No --to specified!\n"; 78 | $self->print_usage; 79 | } 80 | } 81 | 82 | 83 | __PACKAGE__->meta->make_immutable; 84 | no Any::Moose; 85 | 86 | 87 | 88 | 1; 89 | 90 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Server.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Server; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | 5 | has server => ( 6 | is => 'rw', 7 | isa => 'Maybe[Prophet::Server]', 8 | default => sub { 9 | my $self = shift; 10 | return $self->setup_server(); 11 | }, 12 | lazy => 1, 13 | ); 14 | 15 | sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), p => 'port', w => 'writable' }; 16 | 17 | use Prophet::Server; 18 | 19 | sub usage_msg { 20 | my $self = shift; 21 | my ($cmd, $subcmd) = $self->get_cmd_and_subcmd_names( no_type => 1 ); 22 | 23 | return <<"END_USAGE"; 24 | usage: ${cmd}${subcmd} [--port ] 25 | END_USAGE 26 | } 27 | 28 | sub run { 29 | my $self = shift; 30 | 31 | $self->print_usage if $self->has_arg('h'); 32 | 33 | Prophet::CLI->end_pager(); 34 | $self->server->run; 35 | } 36 | 37 | sub setup_server { 38 | my $self = shift; 39 | 40 | my $server_class = ref($self->app_handle) . "::Server"; 41 | if (!$self->app_handle->try_to_require($server_class)) { 42 | $server_class = "Prophet::Server"; 43 | } 44 | my $server; 45 | if ( $self->has_arg('port') ) { 46 | $server = $server_class->new( 47 | app_handle => $self->app_handle, port => $self->arg('port') ); 48 | } 49 | else { 50 | $server = $server_class->new( app_handle => $self->app_handle ); 51 | } 52 | return $server; 53 | } 54 | 55 | 56 | 57 | __PACKAGE__->meta->make_immutable; 58 | no Any::Moose; 59 | 60 | 1; 61 | 62 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Show.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Show; 2 | use Any::Moose; 3 | use Params::Validate; 4 | extends 'Prophet::CLI::Command'; 5 | with 'Prophet::CLI::RecordCommand'; 6 | 7 | sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), 'b' => 'batch' }; 8 | 9 | sub usage_msg { 10 | my $self = shift; 11 | my ($cmd, $type_and_subcmd) = $self->get_cmd_and_subcmd_names; 12 | 13 | return <<"END_USAGE"; 14 | usage: ${cmd}$type_and_subcmd [--batch] [--verbose] 15 | END_USAGE 16 | } 17 | 18 | sub run { 19 | my $self = shift; 20 | 21 | $self->print_usage if $self->has_arg('h'); 22 | 23 | $self->require_uuid; 24 | my $record = $self->_load_record; 25 | 26 | print $self->stringify_props( 27 | record => $record, 28 | batch => $self->has_arg('batch'), 29 | verbose => $self->has_arg('verbose'), 30 | ); 31 | } 32 | 33 | =head2 stringify_props 34 | 35 | Returns a stringified form of the properties suitable for displaying directly 36 | to the user. Also includes luid and uuid. 37 | 38 | =cut 39 | 40 | sub stringify_props { 41 | my $self = shift; 42 | my %args = validate( @_, {record => { ISA => 'Prophet::Record'}, 43 | batch => 1, 44 | verbose => 1}); 45 | 46 | my $record = $args{'record'}; 47 | my $props = $record->get_props; 48 | 49 | 50 | # which props are we going to display? 51 | my @show_props; 52 | if ($record->can('props_to_show')) { 53 | @show_props = $record->props_to_show(\%args); 54 | 55 | # if they ask for verbosity, then display all the other fields 56 | # after the fields that our subclass wants to show 57 | if ($args{verbose}) { 58 | my %already_shown = map { $_ => 1 } @show_props; 59 | push @show_props, grep { !$already_shown{$_} } 60 | sort keys %$props; 61 | } 62 | } 63 | else { 64 | @show_props = ('id', sort keys %$props); 65 | } 66 | 67 | # kind of ugly but it simplifies the code 68 | $props->{id} = $record->luid ." (" . $record->uuid . ")"; 69 | 70 | my @fields; 71 | 72 | for my $field (@show_props) { 73 | my $value = $props->{$field}; 74 | 75 | # don't bother displaying unset fields 76 | next if !defined($value); 77 | 78 | push @fields, [$field, $value]; 79 | 80 | } 81 | 82 | 83 | return join '', 84 | map { 85 | my ($field, $value) = @$_; 86 | $self->format_prop(@$_); 87 | } 88 | @fields; 89 | } 90 | 91 | sub format_prop { 92 | my $self = shift; 93 | my $field = shift; 94 | my $value = shift; 95 | return "$field: $value\n" 96 | } 97 | 98 | __PACKAGE__->meta->make_immutable; 99 | no Any::Moose; 100 | 101 | 1; 102 | 103 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Command/Update.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Command::Update; 2 | use Any::Moose; 3 | extends 'Prophet::CLI::Command'; 4 | with 'Prophet::CLI::RecordCommand'; 5 | 6 | sub ARG_TRANSLATIONS { shift->SUPER::ARG_TRANSLATIONS(), e => 'edit' }; 7 | 8 | sub usage_msg { 9 | my $self = shift; 10 | my ($cmd, $type_and_subcmd) = $self->get_cmd_and_subcmd_names; 11 | 12 | return <<"END_USAGE"; 13 | usage: ${cmd}${type_and_subcmd} --edit 14 | ${cmd}${type_and_subcmd} -- prop1="new value" 15 | END_USAGE 16 | } 17 | 18 | sub edit_record { 19 | my $self = shift; 20 | my $record = shift; 21 | 22 | my $props = $record->get_props; 23 | # don't feed in existing values if we're not interactively editing 24 | my $defaults = $self->has_arg('edit') ? $props : undef; 25 | 26 | my @ordering = ( ); 27 | # we want props in $record->props_to_show to show up in the editor if --edit 28 | # is supplied too 29 | if ($record->can('props_to_show') && $self->has_arg('edit')) { 30 | @ordering = $record->props_to_show; 31 | map { $props->{$_} = '' if !exists($props->{$_}) } @ordering; 32 | } 33 | 34 | return $self->edit_props(arg => 'edit', defaults => $defaults, 35 | ordering => \@ordering); 36 | } 37 | 38 | sub run { 39 | my $self = shift; 40 | 41 | $self->print_usage if $self->has_arg('h'); 42 | 43 | $self->require_uuid; 44 | my $record = $self->_load_record; 45 | 46 | my $new_props = $self->edit_record($record); 47 | 48 | # filter out props that haven't changed 49 | for my $prop (keys %$new_props) { 50 | my $old_prop = defined $record->prop($prop) ? $record->prop($prop) : ''; 51 | delete $new_props->{$prop} if ($old_prop eq $new_props->{$prop}); 52 | } 53 | 54 | if (keys %$new_props) { 55 | my $result = $record->set_props( props => $new_props ); 56 | 57 | if ($result) { 58 | print ucfirst($record->type) . " " . $record->luid . " (".$record->uuid.")"." updated.\n"; 59 | 60 | } else { 61 | print "SOMETHING BAD HAPPENED " 62 | . $record->type . " " 63 | . $record->luid . " (" 64 | . $record->uuid 65 | . ") not updated.\n"; 66 | } 67 | } else { 68 | print "No properties changed.\n"; 69 | } 70 | } 71 | 72 | __PACKAGE__->meta->make_immutable; 73 | no Any::Moose; 74 | 75 | 1; 76 | 77 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Dispatcher/Rule.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Dispatcher::Rule; 2 | use Any::Moose 'Role'; 3 | with 'Prophet::CLI::Parameters'; 4 | 5 | no Any::Moose 'Role'; 6 | 7 | 1; 8 | 9 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Dispatcher/Rule/RecordId.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::Dispatcher::Rule::RecordId; 2 | use Any::Moose; 3 | extends 'Path::Dispatcher::Rule::Regex'; 4 | with 'Prophet::CLI::Dispatcher::Rule'; 5 | 6 | use Prophet::CLIContext; 7 | 8 | has '+regex' => ( 9 | default => sub { qr/^$Prophet::CLIContext::ID_REGEX$/i }, 10 | ); 11 | 12 | has type => ( 13 | is => 'ro', 14 | isa => 'Str', 15 | ); 16 | 17 | sub complete { 18 | my $self = shift; 19 | my $path = shift->path; 20 | 21 | my $handle = $self->cli->app_handle->handle; 22 | 23 | my @types = $self->type || @{ $handle->list_types }; 24 | 25 | my @ids; 26 | for my $type (@types) { 27 | push @ids, 28 | grep { substr($_, 0, length($path)) eq $path } 29 | map { ($_->uuid, $_->luid) } 30 | @{ $handle->list_records( 31 | type => $type, 32 | record_class => $self->cli->record_class, 33 | ) }; 34 | } 35 | return @ids; 36 | } 37 | 38 | __PACKAGE__->meta->make_immutable; 39 | no Any::Moose; 40 | 41 | 1; 42 | 43 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/MirrorCommand.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::MirrorCommand; 2 | use Any::Moose 'Role'; 3 | with 'Prophet::CLI::ProgressBar'; 4 | use Params::Validate ':all'; 5 | 6 | 7 | 8 | sub get_cache_for_source { 9 | my $self = shift; 10 | my ($source) = validate_pos(@_,{isa => 'Prophet::Replica'}); 11 | my $target = Prophet::Replica->get_handle( url => 'prophet_cache:' . $source->uuid , app_handle => $self->app_handle ); 12 | 13 | if ( !$target->replica_exists && !$target->can_initialize ) { 14 | die "The target replica path you specified can't be created.\n"; 15 | } 16 | 17 | $target->initialize_from_source($source); 18 | return $target; 19 | } 20 | 21 | sub sync_cache_from_source { 22 | my $self = shift; 23 | my %args = validate(@_, { target => { isa => 'Prophet::Replica::prophet_cache'}, source => { isa => 'Prophet::Replica'}}); 24 | 25 | if ($args{target}->latest_sequence_no == $args{source}->latest_sequence_no) { 26 | print "Mirror of ".$args{source}->url. " is already up to date\n"; 27 | return 28 | } 29 | 30 | print "Mirroring resolutions from " . $args{source}->url . "\n"; 31 | $args{target}->resolution_db_handle->mirror_from( 32 | source => $args{source}->resolution_db_handle, 33 | reporting_callback => $self->progress_bar( max => ($args{source}->resolution_db_handle->latest_sequence_no ||0) ) 34 | ); 35 | print "\nMirroring changesets from " . $args{source}->url . "\n"; 36 | $args{target}->mirror_from( 37 | source => $args{source}, 38 | reporting_callback => $self->progress_bar( max => ($args{source}->latest_sequence_no ||0) ) 39 | ); 40 | } 41 | 42 | no Any::Moose 'Role'; 43 | 44 | 1; 45 | 46 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/Parameters.pm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | package Prophet::CLI::Parameters; 3 | use Any::Moose 'Role'; 4 | 5 | sub cli { 6 | return $Prophet::CLI::Dispatcher::cli; 7 | } 8 | 9 | sub context { 10 | my $self = shift; 11 | $self->cli->context; 12 | } 13 | 14 | no Any::Moose 'Role'; 15 | 16 | 1; 17 | 18 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/ProgressBar.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::ProgressBar; 2 | use Any::Moose 'Role'; 3 | 4 | use Time::Progress; 5 | use Params::Validate ':all'; 6 | 7 | sub progress_bar { 8 | my $self = shift; 9 | my %args = validate(@_, {max => 1, format => { optional =>1, default => "%30b %p %L (%E remaining)\r" }}); 10 | my $bar = Time::Progress->new(); 11 | 12 | 13 | $bar->attr(max => $args{max}); 14 | my $bar_count = 0; 15 | my $format = $args{format}; 16 | return sub { 17 | # disable autoflush to make \r work properly 18 | local $| = 1; 19 | print $bar->report( $format, ++$bar_count ); 20 | } 21 | } 22 | 23 | no Any::Moose 'Role'; 24 | 25 | 1; 26 | 27 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/PublishCommand.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::PublishCommand; 2 | use Any::Moose 'Role'; 3 | 4 | use File::Temp (); 5 | 6 | sub tempdir { my $dir = File::Temp::tempdir(CLEANUP => ! $ENV{PROPHET_DEBUG} ); return $dir; } 7 | 8 | sub publish_dir { 9 | my $self = shift; 10 | my %args = @_; 11 | 12 | 13 | $args{from} .= '/'; 14 | 15 | my $rsync = $ENV{RSYNC} || "rsync"; 16 | 17 | my @args; 18 | 19 | # chmod feature requires rsync >= 2.6.7 20 | my ($rsync_version) = ( (qx{$rsync --version})[0] =~ /version ([\d.]+) / ); 21 | $rsync_version =~ s/[.]//g if $rsync_version; # kill dot separators in vnum 22 | if ( $rsync_version && $rsync_version < 267 ) { 23 | warn <<'END_WARNING'; 24 | 25 | W: rsync >= 2.6.7 is required in order to ensure the published replica has 26 | W: the default permissions of the destination if they are more permissive 27 | W: than the source replica's permissions. You may wish to upgrade your 28 | W: rsync if possible. (I'll still publish, but your published replica 29 | W: will have the same permissions as the source replica, which is probably 30 | W: not what you want.) 31 | END_WARNING 32 | } 33 | # Set directories to be globally +rx, files to be globally +r 34 | # note - this frobs the permissions on the *sending* side; the 35 | # receiving side's umask is still applied -- this option just 36 | # allows you to publish a replica stored in a private directory 37 | # and have it have the receiving end's default permissions, even 38 | # if those are more permissive than the original location 39 | push @args, '--chmod=Da+rx,a+r'; 40 | 41 | push @args, '--verbose' if $self->context->has_arg('verbose'); 42 | 43 | # avoid edge cases when exporting replicas! still update files even 44 | # if they have the same size and time. 45 | # (latest-sequence-no is a file that can fall into this trap, since it's 46 | # ~easy for it to have the same size as it was previously and in test 47 | # cases we sometimes export to the same directory in quick succession) 48 | push @args, '--ignore-times'; 49 | 50 | if ( $^O =~ /MSWin/ ) { 51 | require Win32; 52 | for (qw/from to/) { 53 | # convert old 8.3 name 54 | $args{$_} = Win32::GetLongPathName($args{$_}); 55 | # cwrsync uses cygwin 56 | $args{$_} =~ s!^([A-Z]):!'/cygdrive/' . lc $1!eg; 57 | $args{$_} =~ s!\\!/!g; 58 | $args{$_} = q{"} . $args{$_} . q{"}; 59 | } 60 | } 61 | 62 | push @args, '-e', $args{shell} if defined $args{shell}; 63 | 64 | push @args, '--recursive', '--' , $args{from}, $args{to}; 65 | 66 | my $ret = system($rsync, @args); 67 | 68 | if ($ret == -1) { 69 | die <<'END_DIE_MSG'; 70 | You must have 'rsync' installed to use this command. 71 | 72 | If you have rsync but it's not in your path, set the environment variable 73 | $RSYNC to the absolute path of your rsync executable. 74 | END_DIE_MSG 75 | } 76 | elsif ($ret != 0) { 77 | die "Publish NOT completed! (rsync failed with return value $ret)\n"; 78 | } 79 | else { 80 | return $ret; 81 | } 82 | } 83 | 84 | no Any::Moose 'Role'; 85 | 86 | 1; 87 | 88 | -------------------------------------------------------------------------------- /lib/Prophet/CLI/RecordCommand.pm: -------------------------------------------------------------------------------- 1 | package Prophet::CLI::RecordCommand; 2 | use Any::Moose 'Role'; 3 | use Params::Validate; 4 | use Prophet::Record; 5 | 6 | 7 | has type => ( 8 | is => 'rw', 9 | isa => 'Str', 10 | required => 0, 11 | predicate => 'has_type', 12 | ); 13 | 14 | has uuid => ( 15 | is => 'rw', 16 | isa => 'Str', 17 | required => 0, 18 | predicate => 'has_uuid', 19 | ); 20 | 21 | has record_class => ( 22 | is => 'rw', 23 | isa => 'Prophet::Record', 24 | ); 25 | 26 | =head2 _get_record_object [{ type => 'type' }] 27 | 28 | Tries to determine a record class from either the given type argument or 29 | the current object's C<$type> attribute. 30 | 31 | Returns a new instance of the record class on success, or throws a fatal 32 | error with a stack trace on failure. 33 | 34 | =cut 35 | 36 | sub _get_record_object { 37 | my $self = shift; 38 | my %args = validate(@_, { 39 | type => { default => $self->type }, 40 | }); 41 | 42 | my $constructor_args = { 43 | app_handle => $self->cli->app_handle, 44 | handle => $self->cli->handle, 45 | type => $args{type}, 46 | }; 47 | 48 | if ($args{type}) { 49 | my $class = $self->_type_to_record_class($args{type}); 50 | return $class->new($constructor_args); 51 | } 52 | elsif (my $class = $self->record_class) { 53 | Prophet::App->require($class); 54 | return $class->new($constructor_args); 55 | } 56 | else { 57 | $self->fatal_error("I couldn't find that record. (You didn't specify a record type.)"); 58 | } 59 | } 60 | 61 | =head2 _load_record 62 | 63 | Attempts to load the record specified by the C attribute. 64 | 65 | Returns the loaded record on success, or throws a fatal error if no 66 | record can be found. 67 | 68 | =cut 69 | 70 | sub _load_record { 71 | my $self = shift; 72 | my $record = $self->_get_record_object; 73 | $record->load( uuid => $self->uuid ); 74 | 75 | if (! $record->exists) { 76 | $self->fatal_error("I couldn't find a " . $self->type . ' with that id.'); 77 | } 78 | return $record; 79 | } 80 | 81 | =head2 _type_to_record_class $type 82 | 83 | Takes a type and tries to figure out a record class name from it. 84 | Returns C<'Prophet::Record'> if no better class name is found. 85 | 86 | =cut 87 | 88 | sub _type_to_record_class { 89 | my $self = shift; 90 | my $type = shift; 91 | my $try = $self->cli->app_class . "::Model::" . ucfirst( lc($type) ); 92 | Prophet::App->try_to_require($try); # don't care about fails 93 | return $try if ( $try->isa('Prophet::Record') ); 94 | 95 | $try = $self->cli->app_class . "::Record"; 96 | Prophet::App->try_to_require($try); # don't care about fails 97 | return $try if ( $try->isa('Prophet::Record') ); 98 | return 'Prophet::Record'; 99 | } 100 | 101 | no Any::Moose 'Role'; 102 | 103 | 1; 104 | 105 | -------------------------------------------------------------------------------- /lib/Prophet/Collection.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Collection; 2 | use Any::Moose; 3 | use Params::Validate; 4 | use Prophet::Record; 5 | 6 | use overload '@{}' => sub { shift->items }, fallback => 1; 7 | use constant record_class => 'Prophet::Record'; 8 | 9 | has app_handle => ( 10 | is => 'rw', 11 | isa => 'Prophet::App|Undef', 12 | required => 0, 13 | trigger => sub { 14 | my ($self, $app) = @_; 15 | $self->handle($app->handle); 16 | }, 17 | ); 18 | 19 | has handle => ( 20 | is => 'rw', 21 | isa => 'Prophet::Replica', 22 | ); 23 | 24 | has type => ( 25 | is => 'rw', 26 | isa => 'Str', 27 | lazy => 1, 28 | default => sub { 29 | my $self = shift; 30 | $self->record_class->new(app_handle => $self->app_handle)->record_type; 31 | }, 32 | ); 33 | 34 | has items => ( 35 | is => 'rw', 36 | isa => 'ArrayRef', 37 | default => sub { [] }, 38 | auto_deref => 1, 39 | ); 40 | 41 | sub count { scalar @{ $_[0]->items } } 42 | sub add_item { 43 | my $self = shift; 44 | push @{ $self->items }, @_; 45 | } 46 | 47 | =head1 NAME 48 | 49 | Prophet::Collection 50 | 51 | =head1 DESCRIPTION 52 | 53 | This class allows the programmer to search for L 54 | objects matching certain criteria and to operate on those records 55 | as a collection. 56 | 57 | =head1 METHODS 58 | 59 | =head2 new { handle => L, type => $TYPE } 60 | 61 | Instantiate a new, empty L object to find items of type 62 | C<$TYPE>. 63 | 64 | =head2 matching $CODEREF 65 | 66 | Find all Ls of this collection's C where $CODEREF 67 | returns true. 68 | 69 | =cut 70 | 71 | sub matching { 72 | my $self = shift; 73 | my $coderef = shift; 74 | # return undef unless $self->handle->type_exists( type => $self->type ); 75 | # find all items, 76 | Carp::cluck unless defined $self->type; 77 | 78 | my $records = $self->handle->list_records( record_class => $self->record_class, type => $self->type ); 79 | 80 | # run coderef against each item; 81 | # if it matches, add it to items 82 | for my $record (@$records) { 83 | $self->add_item($record) if ( $coderef->($record) ); 84 | } 85 | 86 | # XXX TODO return a count of items found 87 | 88 | } 89 | 90 | =head2 items 91 | 92 | Returns a reference to an array of all the items found 93 | 94 | =head2 add_item 95 | 96 | =head2 count 97 | 98 | =cut 99 | 100 | __PACKAGE__->meta->make_immutable; 101 | no Any::Moose; 102 | 103 | 1; 104 | -------------------------------------------------------------------------------- /lib/Prophet/ConflictingChange.pm: -------------------------------------------------------------------------------- 1 | package Prophet::ConflictingChange; 2 | use Any::Moose; 3 | use Prophet::Meta::Types; 4 | use Prophet::ConflictingPropChange; 5 | use JSON 'to_json'; 6 | use Digest::SHA 'sha1_hex'; 7 | 8 | has record_type => ( 9 | is => 'rw', 10 | isa => 'Str', 11 | ); 12 | 13 | has record_uuid => ( 14 | is => 'rw', 15 | isa => 'Str', 16 | ); 17 | 18 | has source_record_exists => ( 19 | is => 'rw', 20 | isa => 'Bool', 21 | ); 22 | 23 | has target_record_exists => ( 24 | is => 'rw', 25 | isa => 'Bool', 26 | ); 27 | 28 | has change_type => ( 29 | is => 'rw', 30 | isa => 'Prophet::Type::ChangeType', 31 | ); 32 | 33 | has file_op_conflict => ( 34 | is => 'rw', 35 | isa => 'Prophet::Type::FileOpConflict', 36 | ); 37 | 38 | has prop_conflicts => ( 39 | is => 'rw', 40 | isa => 'ArrayRef', 41 | default => sub { [] }, 42 | ); 43 | 44 | sub has_prop_conflicts { scalar @{ $_[0]->prop_conflicts } } 45 | sub add_prop_conflict { 46 | my $self = shift; 47 | push @{ $self->prop_conflicts }, @_; 48 | } 49 | 50 | sub as_hash { 51 | my $self = shift; 52 | my $struct = { 53 | map { $_ => $self->$_() } ( 54 | qw/record_type record_uuid source_record_exists target_record_exists change_type file_op_conflict/ 55 | ) 56 | }; 57 | for ( @{ $self->prop_conflicts } ) { 58 | push @{ $struct->{'prop_conflicts'} }, $_->as_hash; 59 | } 60 | 61 | return $struct; 62 | } 63 | 64 | =head2 fingerprint 65 | 66 | Returns a fingerprint of the content of this conflicting change 67 | 68 | =cut 69 | 70 | 71 | sub fingerprint { 72 | my $self = shift; 73 | 74 | my $struct = $self->as_hash; 75 | for ( @{ $struct->{prop_conflicts} } ) { 76 | $_->{choices} = [ sort grep { defined} ( delete $_->{source_new_value}, delete $_->{target_value} ) ]; 77 | } 78 | 79 | return sha1_hex(to_json($struct, {utf8 => 1, canonical => 1})); 80 | } 81 | 82 | __PACKAGE__->meta->make_immutable; 83 | no Any::Moose; 84 | 85 | 1; 86 | -------------------------------------------------------------------------------- /lib/Prophet/ConflictingPropChange.pm: -------------------------------------------------------------------------------- 1 | package Prophet::ConflictingPropChange; 2 | use Any::Moose; 3 | 4 | has name => ( 5 | is => 'rw', 6 | isa => 'Str', 7 | ); 8 | 9 | has source_old_value => ( 10 | is => 'rw', 11 | isa => 'Str|Undef', 12 | ); 13 | 14 | has target_value => ( 15 | is => 'rw', 16 | isa => 'Str|Undef', 17 | ); 18 | 19 | has source_new_value => ( 20 | is => 'rw', 21 | isa => 'Str|Undef', 22 | ); 23 | 24 | =head1 NAME 25 | 26 | Prophet::ConflictingPropChange 27 | 28 | =head1 DESCRIPTION 29 | 30 | Objects of this class describe a case when the a property change can not be cleanly applied to a replica because the old value for the property locally did not match the "begin state" of the change being applied. 31 | 32 | =head1 METHODS 33 | 34 | =head2 name 35 | 36 | The property name for the conflict in question 37 | 38 | =head2 source_old_value 39 | 40 | The inital (old) state from the change being merged in 41 | 42 | =head2 source_new_value 43 | 44 | The final (new) state of the property from the change being merged in. 45 | 46 | =head2 target_value 47 | 48 | The current target-replica value of the property being merged. 49 | 50 | =cut 51 | 52 | sub as_hash { 53 | my $self = shift; 54 | my $hashref = {}; 55 | 56 | for (qw(name source_old_value target_value source_new_value)) { 57 | $hashref->{$_} = $self->$_ 58 | } 59 | return $hashref; 60 | } 61 | 62 | __PACKAGE__->meta->make_immutable; 63 | no Any::Moose; 64 | 65 | 1; 66 | -------------------------------------------------------------------------------- /lib/Prophet/ContentAddressedStore.pm: -------------------------------------------------------------------------------- 1 | package Prophet::ContentAddressedStore; 2 | use Any::Moose; 3 | 4 | use JSON; 5 | use Digest::SHA qw(sha1_hex); 6 | 7 | has fs_root => ( 8 | is => 'rw', 9 | ); 10 | 11 | has root => ( 12 | isa => 'Str', 13 | is => 'rw', 14 | ); 15 | 16 | sub write { 17 | my ($self, $content) = @_; 18 | 19 | $content = $$content 20 | if ref($content) eq 'SCALAR'; 21 | 22 | $content = to_json( $content, 23 | { canonical => 1, pretty => 0, utf8 => 1 } ) 24 | if ref($content); 25 | my $fingerprint = sha1_hex($content); 26 | Prophet::Util->write_file( file => $self->filename($fingerprint, 1), 27 | content => $content ); 28 | 29 | return $fingerprint; 30 | } 31 | 32 | sub filename { 33 | my ($self, $key, $full) = @_; 34 | Prophet::Util->catfile( $full ? $self->fs_root : (), 35 | $self->root => 36 | Prophet::Util::hashed_dir_name($key) ); 37 | } 38 | 39 | __PACKAGE__->meta->make_immutable(); 40 | no Any::Moose; 41 | 1; 42 | -------------------------------------------------------------------------------- /lib/Prophet/DatabaseSetting.pm: -------------------------------------------------------------------------------- 1 | package Prophet::DatabaseSetting; 2 | use Any::Moose; 3 | extends 'Prophet::Record'; 4 | 5 | use Params::Validate; 6 | use JSON; 7 | 8 | has default => ( 9 | is => 'ro', 10 | ); 11 | 12 | has label => ( 13 | isa => 'Str|Undef', 14 | is => 'rw', 15 | ); 16 | 17 | has '+type' => ( default => '__prophet_db_settings' ); 18 | 19 | sub BUILD { 20 | my $self = shift; 21 | 22 | $self->initialize 23 | unless ($self->handle->record_exists(uuid => $self->uuid, type => $self->type) ); 24 | } 25 | 26 | sub initialize { 27 | my $self = shift; 28 | 29 | $self->set($self->default); 30 | } 31 | 32 | sub set { 33 | my $self = shift; 34 | my $entry; 35 | 36 | if (exists $_[1] || !ref($_[0])) { 37 | $entry = [@_]; 38 | } else { 39 | $entry = shift @_; 40 | } 41 | 42 | my $content = to_json($entry, { 43 | canonical => 1, 44 | pretty => 0, 45 | utf8 => 1, 46 | allow_nonref => 0, 47 | }); 48 | 49 | my %props = ( 50 | content => $content, 51 | label => $self->label, 52 | ); 53 | 54 | if ($self->handle->record_exists( uuid => $self->uuid, type => $self->type)) { 55 | $self->set_props(props => \%props); 56 | } 57 | else { 58 | $self->_create_record( 59 | uuid => $self->uuid, 60 | props => \%props, 61 | ); 62 | } 63 | } 64 | 65 | sub get_raw { 66 | my $self = shift; 67 | my $content = $self->prop('content'); 68 | return $content; 69 | } 70 | 71 | sub get { 72 | my $self = shift; 73 | 74 | $self->initialize() unless $self->load(uuid => $self->uuid); 75 | my $content = $self->get_raw; 76 | 77 | my $entry = from_json($content, { utf8 => 1 }); 78 | return $entry; 79 | } 80 | 81 | __PACKAGE__->meta->make_immutable; 82 | no Any::Moose; 83 | 84 | 1; 85 | 86 | -------------------------------------------------------------------------------- /lib/Prophet/Meta/Types.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Meta::Types; 2 | use Any::Moose; 3 | use Any::Moose 'Util::TypeConstraints'; 4 | 5 | enum 'Prophet::Type::ChangeType' => qw/add_file add_dir update_file delete/; 6 | enum 'Prophet::Type::FileOpConflict' => qw/delete_missing_file update_missing_file create_existing_file create_existing_dir/; 7 | 8 | __PACKAGE__->meta->make_immutable; 9 | no Any::Moose; 10 | 11 | 1; 12 | 13 | __END__ 14 | 15 | =head1 NAME 16 | 17 | Prophet::Meta::Types - extra types for Prophet 18 | 19 | =head1 TYPES 20 | 21 | =head2 Prophet::Type::ChangeType 22 | 23 | A single change type: add_file, add_dir, update_file, delete. 24 | 25 | =cut 26 | 27 | -------------------------------------------------------------------------------- /lib/Prophet/PropChange.pm: -------------------------------------------------------------------------------- 1 | package Prophet::PropChange; 2 | use Any::Moose; 3 | 4 | has name => ( 5 | is => 'rw', 6 | isa => 'Str', 7 | ); 8 | 9 | has old_value => ( 10 | is => 'rw', 11 | isa => 'Str|Undef', 12 | ); 13 | 14 | has new_value => ( 15 | is => 'rw', 16 | isa => 'Str|Undef', 17 | ); 18 | 19 | =head1 NAME 20 | 21 | Prophet::PropChange 22 | 23 | =head1 DESCRIPTION 24 | 25 | This class encapsulates a single property change. 26 | 27 | =head1 METHODS 28 | 29 | =head2 name 30 | 31 | The name of the property we're talking about. 32 | 33 | =head2 old_value 34 | 35 | What L changed I. 36 | 37 | =head2 new_value 38 | 39 | What L changed I. 40 | 41 | 42 | =cut 43 | 44 | sub summary { 45 | my $self = shift; 46 | my $name = $self->name || '(property name missing)'; 47 | my $old = $self->old_value; 48 | my $new = $self->new_value; 49 | 50 | if (!defined($old)) { 51 | return qq{+ "$name" set to "}.($new||'').qq{"}; 52 | } 53 | elsif (!defined($new)) { 54 | return qq{- "$name" "$old" deleted.}; 55 | } 56 | 57 | return qq{> "$name" changed from "$old" to "$new".}; 58 | } 59 | 60 | __PACKAGE__->meta->make_immutable; 61 | no Any::Moose; 62 | 63 | 1; 64 | -------------------------------------------------------------------------------- /lib/Prophet/Replica/FS/Backend/File.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Replica::FS::Backend::File; 2 | use Any::Moose; 3 | use Fcntl qw/SEEK_END/; 4 | use Params::Validate qw/validate validate_pos/; 5 | 6 | 7 | has url => ( is => 'rw', isa => 'Str'); 8 | has fs_root => ( is => 'rw', isa => 'Str'); 9 | 10 | sub read_file { 11 | my $self = shift; 12 | my ($file) = (@_); # validation is too heavy to be called here 13 | #my ($file) = validate_pos( @_, 1 ); 14 | return eval { 15 | local $SIG{__DIE__} = 'DEFAULT'; 16 | Prophet::Util->slurp( 17 | Prophet::Util->catfile( $self->fs_root => $file ) ); 18 | }; 19 | } 20 | 21 | sub read_file_range { 22 | my $self = shift; 23 | my %args = validate( @_, { path => 1, position => 1, length => 1 } ); 24 | 25 | if ($self->fs_root) { 26 | my $f = Prophet::Util->catfile( $self->fs_root => $args{path} ); 27 | return unless -e $f; 28 | if ( $^O =~ /MSWin/ ) { 29 | # XXX by sunnavy 30 | # the the open, seek and read below doesn't work on windows, at least with 31 | # strawberry perl 5.10.0.6 on windows xp 32 | # 33 | # the differences: 34 | # with substr, I got: 35 | # 0000000: 0000 0004 ecaa d794 a5fe 8c6f 6e85 0d0a ...........on... 36 | # 0000010: 7087 f0cf 1e92 b50d f9 p........ 37 | # 38 | # the read, I got 39 | # 0000000: 0000 04ec aad7 94a5 fe8c 6f6e 850d 0d0a ..........on.... 40 | # 0000010: 7087 f0cf 1e92 b50d f9 p........ 41 | # 42 | # seems with read, we got an extra 0d, I dont' know why yet :/ 43 | my $content = Prophet::Util->slurp( $f ); 44 | return substr($content, $args{position}, $args{length}); 45 | } 46 | else { 47 | open( my $index, "<:bytes", $f ) or return; 48 | seek( $index, $args{position}, SEEK_END ) or return; 49 | my $record; 50 | read( $index, $record, $args{length} ) or return; 51 | return $record; 52 | } 53 | } 54 | else { 55 | # XXX: do range get if possible 56 | my $content = $self->lwp_get( $self->url . "/" . $args{path} ); 57 | return substr($content, $args{position}, $args{length}); 58 | } 59 | 60 | } 61 | 62 | sub write_file { 63 | my $self = shift; 64 | my %args = (@_); # validation is too heavy to call here 65 | #my %args = validate( @_, { path => 1, content => 1 } ); 66 | 67 | my $file = Prophet::Util->catfile( $self->fs_root => $args{'path'} ); 68 | Prophet::Util->write_file( file => $file, content => $args{content}); 69 | 70 | } 71 | 72 | sub append_to_file { 73 | my $self = shift; 74 | my ($filename, $content) = validate_pos(@_, 1,1 ); 75 | open( my $file, 76 | ">>" . Prophet::Util->catfile( $self->fs_root => $filename) 77 | ) || die $!; 78 | print $file $content || die $!; 79 | close $file; 80 | } 81 | 82 | sub file_exists { 83 | my $self = shift; 84 | my ($file) = validate_pos( @_, 1 ); 85 | 86 | 87 | my $path = Prophet::Util->catfile( $self->fs_root, $file ); 88 | if ( -f $path ) { return 1 } 89 | elsif ( -d $path ) { return 2 } 90 | else { return 0 } 91 | 92 | 93 | } 94 | 95 | 96 | sub can_read { 1; 97 | 98 | } 99 | 100 | sub can_write { 1; 101 | 102 | } 103 | 104 | no Any::Moose; 105 | 106 | 1; 107 | -------------------------------------------------------------------------------- /lib/Prophet/Replica/FS/Backend/LWP.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Replica::FS::Backend::LWP; 2 | use Any::Moose; 3 | use Params::Validate qw/validate validate_pos/; 4 | use LWP::UserAgent; 5 | 6 | has url => ( is => 'rw', isa => 'Str'); 7 | 8 | has lwp_useragent => ( 9 | isa => 'LWP::UserAgent', 10 | is => 'ro', 11 | lazy => 1, 12 | default => sub { 13 | my $ua = LWP::UserAgent->new( timeout => 60, keep_alive => 4, agent => "Prophet/".$Prophet::VERSION); 14 | return $ua; 15 | } 16 | ); 17 | 18 | sub read_file { 19 | my $self = shift; 20 | my ($file) = validate_pos( @_, 1 ); 21 | 22 | return $self->lwp_get( $self->url . "/" . $file ); 23 | } 24 | 25 | sub read_file_range { 26 | my $self = shift; 27 | my %args = validate( @_, { path => 1, position => 1, length => 1 } ); 28 | 29 | # XXX: do range get if possible 30 | my $content = $self->lwp_get( $self->url . "/" . $args{path} ); 31 | return substr($content, $args{position}, $args{length}); 32 | 33 | } 34 | 35 | sub lwp_get { 36 | my $self = shift; 37 | my $url = shift; 38 | 39 | my $response; 40 | for ( 1 .. 4 ) { 41 | $response = $self->lwp_useragent->get($url); 42 | if ( $response->is_success ) { 43 | return $response->content; 44 | } 45 | } 46 | warn "Could not fetch " . $url . " - " . $response->status_line . "\n"; 47 | return undef; 48 | } 49 | 50 | 51 | sub write_file { 52 | 53 | } 54 | 55 | sub append_to_file { 56 | 57 | } 58 | 59 | sub file_exists { 60 | my $self = shift; 61 | my ($file) = validate_pos( @_, 1 ); 62 | return defined $self->read_file($file) ? 1 : 0; 63 | } 64 | 65 | 66 | sub can_read { 1; 67 | 68 | } 69 | 70 | sub can_write { 0; 71 | 72 | } 73 | 74 | no Any::Moose; 75 | 76 | 1; 77 | -------------------------------------------------------------------------------- /lib/Prophet/Replica/FS/Backend/SSH.pm: -------------------------------------------------------------------------------- 1 | package Prophet::FSR::Backend::SSH; 2 | use Any::Moose; 3 | 4 | no Any::Moose; 5 | 6 | 1; 7 | -------------------------------------------------------------------------------- /lib/Prophet/Replica/file.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Replica::file; 2 | use Any::Moose; 3 | extends 'Prophet::Replica::prophet'; 4 | sub scheme { 'file' } 5 | 6 | sub replica_exists { 7 | my $self = shift; 8 | return 0 unless defined $self->fs_root && -d $self->fs_root; 9 | return 0 unless -e Prophet::Util->catfile( $self->fs_root => 'database-uuid' ); 10 | return 1; 11 | } 12 | 13 | sub new { 14 | my $class = shift; 15 | my %args = @_; 16 | 17 | my @probe_types = ($args{app_handle}->default_replica_type, 'file', 'sqlite'); 18 | 19 | my %possible; 20 | for my $type (@probe_types) { 21 | my $ret; 22 | eval { 23 | my $other = "Prophet::Replica::$type"; 24 | Prophet::App->try_to_require($other); 25 | $ret = $type eq "file" ? $other->SUPER::new(@_) : $other->new(@_); 26 | }; 27 | next if $@ or not $ret; 28 | return $ret if $ret->replica_exists; 29 | $possible{$type} = $ret; 30 | } 31 | if (my $default_type = $possible{$args{app_handle}->default_replica_type} ) { 32 | return $default_type; 33 | } else { 34 | $class->log_fatal("I don't know what to do with the Prophet replica ". 35 | "type you specified: ".$args{app_handle}->default_replica_type. 36 | "\nIs your URL syntax correct?"); 37 | } 38 | } 39 | 40 | no Any::Moose; 41 | 1; 42 | -------------------------------------------------------------------------------- /lib/Prophet/Replica/http.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Replica::http; 2 | use base 'Prophet::Replica::prophet'; 3 | sub scheme { 'http'} 4 | 5 | 1; 6 | -------------------------------------------------------------------------------- /lib/Prophet/ReplicaExporter.pm: -------------------------------------------------------------------------------- 1 | package Prophet::ReplicaExporter; 2 | use Any::Moose; 3 | use Params::Validate qw(:all); 4 | use File::Spec; 5 | use Prophet::Record; 6 | use Prophet::Collection; 7 | 8 | has source_replica => ( 9 | is => 'rw', 10 | isa => 'Prophet::Replica', 11 | ); 12 | 13 | has target_path => ( 14 | is => 'rw', 15 | isa => 'Str', 16 | predicate => 'has_target_path', 17 | ); 18 | 19 | has target_replica => ( 20 | is => 'rw', 21 | isa => 'Prophet::Replica', 22 | lazy => 1, 23 | default => sub { 24 | my $self = shift; 25 | confess "No target_path specified" unless $self->has_target_path; 26 | my $replica = Prophet::Replica->get_handle(url => "prophet:file://" . $self->target_path, app_handle => $self->app_handle); 27 | 28 | my $src = $self->source_replica; 29 | my %init_args = ( 30 | db_uuid => $src->db_uuid, 31 | ); 32 | 33 | $init_args{resdb_uuid} = $src->resolution_db_handle->db_uuid 34 | if !$src->is_resdb; 35 | 36 | $replica->initialize(%init_args); 37 | 38 | return $replica; 39 | }, 40 | ); 41 | 42 | has app_handle => ( 43 | is => 'ro', 44 | isa => 'Prophet::App', 45 | weak_ref => 1, 46 | predicate => 'has_app_handle', 47 | ); 48 | 49 | =head1 NAME 50 | 51 | Prophet::ReplicaExporter 52 | 53 | =head1 DESCRIPTION 54 | 55 | A utility class which exports a replica to a serialized on-disk format 56 | 57 | =cut 58 | 59 | =head1 METHODS 60 | 61 | =head2 new 62 | 63 | Instantiates a new replica exporter object 64 | 65 | =cut 66 | 67 | =head2 export 68 | 69 | This routine will export a copy of this prophet database replica to a 70 | flat file on disk suitable for publishing via HTTP or over a local 71 | filesystem for other Prophet replicas to clone or incorporate changes 72 | from. 73 | 74 | =cut 75 | 76 | sub export { 77 | my $self = shift; 78 | 79 | $self->init_export_metadata(); 80 | print " Exporting records...\n"; 81 | $self->export_all_records(); 82 | print " Exporting changesets...\n"; 83 | $self->export_changesets(); 84 | 85 | unless ($self->source_replica->is_resdb) { 86 | my $resolutions = Prophet::ReplicaExporter->new( 87 | target_path => File::Spec->catdir($self->target_path, 'resolutions' ), 88 | source_replica => $self->source_replica->resolution_db_handle, 89 | app_handle => $self->app_handle 90 | 91 | ); 92 | print "Exporting resolution database\n"; 93 | $resolutions->export(); 94 | } 95 | } 96 | 97 | sub init_export_metadata { 98 | my $self = shift; 99 | $self->target_replica->set_latest_sequence_no( 100 | $self->source_replica->latest_sequence_no ); 101 | $self->target_replica->set_replica_uuid( $self->source_replica->uuid ); 102 | 103 | } 104 | 105 | sub export_all_records { 106 | my $self = shift; 107 | $self->export_records( type => $_ ) for ( @{ $self->source_replica->list_types } ); 108 | } 109 | 110 | sub export_records { 111 | my $self = shift; 112 | my %args = validate( @_, { type => 1 } ); 113 | 114 | my $collection = Prophet::Collection->new( 115 | app_handle => $self->app_handle, 116 | handle => $self->source_replica, 117 | type => $args{type} 118 | ); 119 | $collection->matching( sub {1} ); 120 | $self->target_replica->_write_record( record => $_ ) for @$collection; 121 | 122 | } 123 | 124 | sub export_changesets { 125 | my $self = shift; 126 | 127 | for my $changeset ( 128 | @{ $self->source_replica->fetch_changesets( after => 0 ) } ) 129 | { 130 | $self->target_replica->_write_changeset( 131 | changeset => $changeset 132 | ); 133 | 134 | } 135 | } 136 | 137 | __PACKAGE__->meta->make_immutable; 138 | no Any::Moose; 139 | 140 | 1; 141 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver; 2 | use Any::Moose; 3 | 4 | __PACKAGE__->meta->make_immutable; 5 | no Any::Moose; 6 | 7 | 1; 8 | 9 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver/AlwaysSource.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver::AlwaysSource; 2 | use Any::Moose; 3 | use Prophet::Change; 4 | extends 'Prophet::Resolver'; 5 | 6 | sub run { 7 | my $self = shift; 8 | my $conflicting_change = shift; 9 | return 0 if $conflicting_change->file_op_conflict; 10 | 11 | my $resolution = Prophet::Change->new_from_conflict($conflicting_change); 12 | return $resolution; 13 | } 14 | 15 | __PACKAGE__->meta->make_immutable; 16 | no Any::Moose; 17 | 18 | 1; 19 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver/AlwaysTarget.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver::AlwaysTarget; 2 | use Any::Moose; 3 | use Data::Dumper; 4 | extends 'Prophet::Resolver'; 5 | 6 | sub run { 7 | my $self = shift; 8 | my $conflicting_change = shift; 9 | my $conflict = shift; 10 | my $resolution = Prophet::Change->new_from_conflict($conflicting_change); 11 | my $file_op_conflict = $conflicting_change->file_op_conflict || ''; 12 | if ( $file_op_conflict eq 'update_missing_file' ) { 13 | $resolution->change_type('delete'); 14 | return $resolution; 15 | } elsif ( $file_op_conflict eq 'delete_missing_file' ) { 16 | return $resolution; 17 | } elsif ( $file_op_conflict ) { 18 | die "Unknown file_op_conflict $file_op_conflict: " . Dumper($conflict,$conflicting_change); 19 | } 20 | 21 | for my $prop_change ( @{ $conflicting_change->prop_conflicts } ) { 22 | $resolution->add_prop_change( 23 | name => $prop_change->name, 24 | old => $prop_change->source_new_value, 25 | new => $prop_change->target_value 26 | ); 27 | } 28 | return $resolution; 29 | } 30 | 31 | __PACKAGE__->meta->make_immutable; 32 | no Any::Moose; 33 | 34 | 1; 35 | 36 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver/Failed.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver::Failed; 2 | use Any::Moose; 3 | use Data::Dumper; 4 | extends 'Prophet::Resolver'; 5 | $Data::Dumper::Indent = 1; 6 | 7 | 8 | sub run { 9 | my $self = shift; 10 | my $conflicting_change = shift; 11 | my $conflict = shift; 12 | 13 | die 14 | "The conflict was not resolved! Sorry dude." 15 | . Dumper($conflict, $conflicting_change); 16 | 17 | } 18 | 19 | __PACKAGE__->meta->make_immutable; 20 | no Any::Moose; 21 | 22 | 1; 23 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver/Fixup/MissingSourceOldValues.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver::Fixup::MissingSourceOldValues; 2 | use Any::Moose; 3 | extends 'Prophet::Resolver'; 4 | 5 | sub run { 6 | my $self = shift; 7 | my $conflicting_change = shift; 8 | return 0 if $conflicting_change->file_op_conflict; 9 | 10 | my $resolution = Prophet::Change->new_from_conflict($conflicting_change); 11 | for my $prop_conflict ( @{ $conflicting_change->prop_conflicts } ) { 12 | 13 | if ( defined $prop_conflict->source_old_value 14 | && $prop_conflict->source_old_value ne '' ) { 15 | return 0; 16 | } 17 | 18 | #$resolution->add_prop_change( name => $prop_conflict->name, old => $prop_conflict->target_value, new => $prop_conflict->source_new_value); 19 | } 20 | return $resolution; 21 | } 22 | 23 | __PACKAGE__->meta->make_immutable; 24 | no Any::Moose; 25 | 26 | 1; 27 | 28 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver/FromResolutionDB.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver::FromResolutionDB; 2 | use Any::Moose; 3 | use Prophet::Change; 4 | use Prophet::Collection; 5 | use JSON; 6 | use Digest::SHA 'sha1_hex'; 7 | extends 'Prophet::Resolver'; 8 | 9 | sub run { 10 | my $self = shift; 11 | my $conflicting_change = shift; 12 | my $conflict = shift; 13 | my $resdb = shift; # XXX: we want diffrent collection actually now 14 | 15 | require Prophet::Collection; 16 | 17 | my $res = Prophet::Collection->new( 18 | handle => $resdb, 19 | # XXX TODO PULL THIS TYPE FROM A CONSTANT 20 | type => '_prophet_resolution-' . $conflicting_change->fingerprint 21 | ); 22 | $res->matching( sub {1} ); 23 | return unless $res->count; 24 | 25 | my %answer_map; 26 | my %answer_count; 27 | 28 | for my $answer ($res->items) { 29 | my $key = sha1_hex( to_json($answer->get_props, {utf8 => 1, pretty => 1, canonical => 1})); 30 | $answer_map{$key} ||= $answer; 31 | $answer_count{$key}++; 32 | } 33 | my $best = ( sort { $answer_count{$b} <=> $answer_count{$a} } keys %answer_map )[0]; 34 | 35 | my $answer = $answer_map{$best}; 36 | 37 | my $resolution = Prophet::Change->new_from_conflict($conflicting_change); 38 | for my $prop_conflict ( @{ $conflicting_change->prop_conflicts } ) { 39 | $resolution->add_prop_change( 40 | name => $prop_conflict->name, 41 | old => $prop_conflict->source_old_value, 42 | new => $answer->prop( $prop_conflict->name ), 43 | ); 44 | } 45 | return $resolution; 46 | } 47 | 48 | __PACKAGE__->meta->make_immutable; 49 | no Any::Moose; 50 | 51 | 1; 52 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver/IdenticalChanges.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver::IdenticalChanges; 2 | use Any::Moose; 3 | use Params::Validate qw(:all); 4 | use Prophet::Change; 5 | extends 'Prophet::Resolver'; 6 | 7 | =head1 METHODS 8 | 9 | =head2 attempt_automatic_conflict_resolution 10 | 11 | Given a L which can not be cleanly applied to a 12 | replica, it is sometimes possible to automatically determine a sane 13 | resolution to the conflict. 14 | 15 | =over 16 | 17 | =item * 18 | 19 | When the new-state of the conflicting change matches the 20 | previous head of the replica. 21 | 22 | =item * 23 | 24 | When someone else has previously done the resolution and we 25 | have a copy of that hanging around. 26 | 27 | =back 28 | 29 | In those cases, this routine will generate a L 30 | which resolves as many conflicts as possible. 31 | 32 | It will then update the conclicting changes to mark which 33 | Ls and L 34 | have been automatically resolved. 35 | 36 | =cut 37 | 38 | sub run { 39 | my $self = shift; 40 | my ( $conflicting_change, $conflict, $resdb ) 41 | = validate_pos( @_, { isa => 'Prophet::ConflictingChange' }, { isa => 'Prophet::Conflict' }, 0 ); 42 | 43 | # for everything from the changeset that is the same as the old value of the target replica 44 | # we can skip applying 45 | return 0 if $conflicting_change->file_op_conflict; 46 | 47 | my $resolution = Prophet::Change->new_from_conflict($conflicting_change); 48 | 49 | for my $prop_change ( @{ $conflicting_change->prop_conflicts } ) { 50 | next if ((!defined $prop_change->target_value || $prop_change->target_value eq '') 51 | 52 | && ( !defined $prop_change->source_new_value || $prop_change->source_new_value eq '')); 53 | next if (defined $prop_change->target_value 54 | and defined $prop_change->source_new_value 55 | and ( $prop_change->target_value eq $prop_change->source_new_value)); 56 | return 0; 57 | } 58 | 59 | $conflict->autoresolved(1); 60 | 61 | return $resolution; 62 | 63 | } 64 | 65 | __PACKAGE__->meta->make_immutable; 66 | no Any::Moose; 67 | 68 | 1; 69 | -------------------------------------------------------------------------------- /lib/Prophet/Resolver/Prompt.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Resolver::Prompt; 2 | use Any::Moose; 3 | extends 'Prophet::Resolver'; 4 | 5 | sub run { 6 | my $self = shift; 7 | my $conflicting_change = shift; 8 | return 0 if $conflicting_change->file_op_conflict; 9 | 10 | my $resolution = Prophet::Change->new_from_conflict($conflicting_change); 11 | print "Oh no! There's a conflict between this replica and the one you're syncing from:\n"; 12 | print $conflicting_change->record_type . " " . $conflicting_change->record_uuid . "\n"; 13 | 14 | for my $prop_conflict ( @{ $conflicting_change->prop_conflicts } ) { 15 | 16 | print $prop_conflict->name . ": \n"; 17 | 18 | my %values; 19 | for (qw/target_value source_old_value source_new_value/) { 20 | $values{$_} = $prop_conflict->$_; 21 | $values{$_} = "(undefined)" 22 | if !defined($values{$_}); 23 | } 24 | 25 | 26 | print "(T)ARGET $values{target_value}\n"; 27 | print "SOURCE (O)LD $values{source_old_value}\n"; 28 | print "SOURCE (N)EW $values{source_new_value}\n"; 29 | 30 | while ( my $choice = lc( substr( || 'T', 0, 1 ) ) ) { 31 | 32 | if ( $choice eq 't' ) { 33 | 34 | $resolution->add_prop_change( 35 | name => $prop_conflict->name, 36 | old => $prop_conflict->source_new_value, 37 | new => $prop_conflict->target_value 38 | ); 39 | last; 40 | } elsif ( $choice eq 'o' ) { 41 | 42 | $resolution->add_prop_change( 43 | name => $prop_conflict->name, 44 | old => $prop_conflict->source_new_value, 45 | new => $prop_conflict->source_old_value 46 | ); 47 | last; 48 | 49 | } elsif ( $choice eq 'n' ) { 50 | last; 51 | 52 | } else { 53 | print "(T), (O) or (N)? "; 54 | } 55 | } 56 | } 57 | return $resolution; 58 | } 59 | 60 | __PACKAGE__->meta->make_immutable; 61 | no Any::Moose; 62 | 63 | 1; 64 | 65 | -------------------------------------------------------------------------------- /lib/Prophet/Server/Dispatcher.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Server::Dispatcher; 2 | use Any::Moose; 3 | use Path::Dispatcher::Declarative -base, -default => { 4 | token_delimiter => '/', 5 | }; 6 | 7 | has server => ( isa => 'Prophet::Server', is => 'rw', weak_ref => 1 ); 8 | 9 | under { method => 'POST' } => sub { 10 | on qr'.*' => sub { 11 | my $self = shift; 12 | return $self->server->_send_401 if ( $self->server->read_only ); 13 | next_rule; 14 | }; 15 | 16 | under qr'/records' => sub { 17 | on qr|^/(.*)/(.*)/(.*)$| => sub { shift->server->update_record_prop($1,$2,$3) }; 18 | on qr|^/(.*)/(.*).json$| => sub { shift->server->update_record($1,$2) }; 19 | on qr|^/(.*).json$| => sub { shift->server->create_record($1) }; 20 | }; 21 | }; 22 | 23 | under { method => 'GET' } => sub { 24 | on qr'^/=/prophet/autocomplete' => sub { 25 | shift->server->show_template('/_prophet_autocompleter') }; 26 | on qr'^/static/prophet/(.*)$' => sub { shift->server->send_static_file($1)}; 27 | 28 | on qr'^/records.json' => sub { shift->server->get_record_types }; 29 | under qr'/records' => sub { 30 | on qr|^/(.*)/(.*)/(.*)$| => sub { shift->server->get_record_prop($1,$2,$3); }; 31 | on qr|^/(.*)/(.*).json$| => sub { shift->server->get_record($1,$2) }; 32 | on qr|^/(.*).json$| => sub { shift->server->get_record_list($1) }; 33 | 34 | }; 35 | 36 | on qr'^/replica(/resolutions)?' => sub { 37 | my $self = shift; 38 | if ($1 && $1 eq '/resolutions') { 39 | $_->metadata->{replica_handle} = $self->server->app_handle->handle->resolution_db_handle; 40 | } else { 41 | $_->metadata->{replica_handle} = $self->server->app_handle->handle; 42 | } 43 | next_rule; 44 | }; 45 | 46 | under qr'^/replica(/resolutions/)?' => sub { 47 | on 'replica-version' => sub { shift->server->send_replica_content('1')}; 48 | on 'replica-uuid' => sub { my $self = shift; $self->server->send_replica_content( $_->metadata->{replica_handle}->uuid ); }; 49 | on 'database-uuid' => sub { my $self = shift; $self->server->send_replica_content( $_->metadata->{replica_handle}->db_uuid ); }; 50 | on 'latest-sequence-no' => sub { my $self = shift; $self->server->send_replica_content( $_->metadata->{replica_handle}->latest_sequence_no ); }; 51 | 52 | on 'changesets.idx' => sub { 53 | my $self = shift; 54 | my $index = ''; 55 | my $repl = $_->metadata->{replica_handle}; 56 | $repl->traverse_changesets( 57 | after=> 0, 58 | load_changesets => 0, 59 | callback => sub { 60 | my %args = (@_); 61 | my $data = $args{changeset_metadata}; 62 | my $changeset_index_line = pack( 'Na16NH40', 63 | $data->[0], 64 | $repl->uuid_generator->from_string( $data->[1]), 65 | $data->[2], 66 | $data->[3]); 67 | $index .= $changeset_index_line; 68 | } 69 | ); 70 | $self->server->send_replica_content($index); 71 | }; 72 | on qr|cas/changesets/././(.{40})$| => sub { 73 | my $self = shift; 74 | my $sha1 = $1; 75 | $self->server->send_replica_content($_->metadata->{replica_handle}->fetch_serialized_changeset(sha1 => $sha1)); 76 | } ; 77 | 78 | 79 | }; 80 | }; 81 | 82 | on qr'^(.*)$' => sub { shift->server->show_template($1) || next_rule; }; 83 | 84 | __PACKAGE__->meta->make_immutable; 85 | no Any::Moose; 86 | 87 | 1; 88 | -------------------------------------------------------------------------------- /lib/Prophet/Server/ViewHelpers.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Server::ViewHelpers; 2 | 3 | use warnings; 4 | use strict; 5 | 6 | use base 'Exporter::Lite'; 7 | use Params::Validate qw/validate/; 8 | use Template::Declare::Tags; 9 | use Prophet::Web::Field; 10 | our @EXPORT = (qw(form page content widget function param_from_function hidden_param)); 11 | use Prophet::Server::ViewHelpers::Widget; 12 | use Prophet::Server::ViewHelpers::Function; 13 | use Prophet::Server::ViewHelpers::ParamFromFunction; 14 | use Prophet::Server::ViewHelpers::HiddenParam; 15 | 16 | 17 | sub page (&;$) { 18 | unshift @_, undef if $#_ == 0; 19 | my ( $meta, $code ) = @_; 20 | 21 | sub { 22 | my $self = shift; 23 | my @args = @_; 24 | my $title = $self->default_page_title; 25 | $title = $meta->( $self, @args ) if $meta; 26 | html { 27 | attr { xmlns => 'http://www.w3.org/1999/xhtml' }; 28 | show( 'head' => $title ); 29 | body { 30 | div { 31 | class is 'page'; 32 | show('header', $title); 33 | div { class is 'body'; 34 | $code->( $self, @args ); 35 | } 36 | } 37 | 38 | }; 39 | show('footer'); 40 | } 41 | 42 | } 43 | } 44 | 45 | sub content (&) { 46 | my $sub_ref = shift; 47 | return $sub_ref; 48 | } 49 | 50 | sub function { 51 | my $f = Prophet::Server::ViewHelpers::Function->new(@_); 52 | $f->render; 53 | return $f; 54 | } 55 | 56 | sub param_from_function { 57 | my $w = Prophet::Server::ViewHelpers::ParamFromFunction->new(@_); 58 | $w->render; 59 | return $w; 60 | } 61 | 62 | sub hidden_param { 63 | my $w = Prophet::Server::ViewHelpers::HiddenParam->new(@_); 64 | $w->render; 65 | return $w; 66 | } 67 | sub widget { 68 | my $w = Prophet::Server::ViewHelpers::Widget->new(@_); 69 | $w->render; 70 | return $w; 71 | } 72 | 73 | BEGIN { 74 | no warnings 'redefine'; 75 | *old_form = \&form; 76 | *form = sub (&;$){ 77 | my $code = shift; 78 | old_form ( sub { attr { method => 'post'}; 79 | $code->(@_); 80 | } 81 | ) 82 | }}; 83 | 84 | 85 | 1; 86 | -------------------------------------------------------------------------------- /lib/Prophet/Server/ViewHelpers/Function.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Server::ViewHelpers::Function; 2 | 3 | use Template::Declare::Tags; 4 | BEGIN { delete ${__PACKAGE__."::"}{meta}; 5 | delete ${__PACKAGE__."::"}{with}; 6 | } 7 | 8 | =head1 NAME 9 | 10 | =head1 METHODS 11 | 12 | =head1 DESCRIPTION 13 | 14 | =cut 15 | 16 | =head1 METHODS 17 | 18 | =cut 19 | 20 | use Any::Moose; 21 | use Any::Moose 'Util::TypeConstraints'; 22 | 23 | 24 | has record => ( 25 | isa => 'Prophet::Record', 26 | is => 'ro' 27 | ); 28 | 29 | has action => ( 30 | isa => ( enum [qw(create update delete)] ), 31 | is => 'ro' 32 | ); 33 | 34 | has order => ( isa => 'Int', is => 'ro' ); 35 | 36 | has validate => ( isa => 'Bool', is => 'rw', default => 1); 37 | has canonicalize => ( isa => 'Bool', is => 'rw', default => 1); 38 | has execute => ( isa => 'Bool', is => 'rw', default => 1); 39 | 40 | 41 | has name => ( 42 | isa => 'Str', 43 | is => 'rw', 44 | 45 | #regex => qr/^(?:|[\w\d]+)$/, 46 | ); 47 | 48 | 49 | 50 | 51 | 52 | sub new { 53 | my $self = shift->SUPER::new(@_); 54 | $self->name ( ($self->record->loaded ? $self->record->uuid : 'new') . "-" . $self->action ) unless ($self->name); 55 | return $self; 56 | } 57 | 58 | sub render { 59 | my $self = shift; 60 | my %bits = ( 61 | order => $self->order, 62 | action => $self->action, 63 | type => $self->record->type, 64 | class => ref($self->record), 65 | uuid => $self->record->uuid, 66 | validate => $self->validate, 67 | canonicalize => $self->canonicalize, 68 | execute => $self->execute 69 | ); 70 | 71 | my $string 72 | = "|" 73 | . join( "|", map { $bits{$_} ? $_ . "=" . $bits{$_} : '' } keys %bits ) 74 | . "|"; 75 | 76 | 77 | outs_raw(qq{}); 78 | } 79 | 80 | 81 | __PACKAGE__->meta->make_immutable(inline_constructor => 0); 82 | no Any::Moose; 83 | 1; 84 | 85 | -------------------------------------------------------------------------------- /lib/Prophet/Server/ViewHelpers/HiddenParam.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Server::ViewHelpers::HiddenParam; 2 | 3 | use Template::Declare::Tags; 4 | 5 | BEGIN { delete ${__PACKAGE__."::"}{meta}; 6 | delete ${__PACKAGE__."::"}{with}; 7 | } 8 | 9 | use Any::Moose; 10 | 11 | extends 'Prophet::Server::ViewHelpers::Widget'; 12 | 13 | 14 | use Any::Moose 'Util::TypeConstraints'; 15 | 16 | 17 | has value => ( isa => 'Str', is => 'rw'); 18 | 19 | 20 | sub render { 21 | my $self = shift; 22 | 23 | my $unique_name = $self->_generate_name(); 24 | 25 | my $record = $self->function->record; 26 | 27 | $self->field( Prophet::Web::Field->new( 28 | name => $unique_name, 29 | id => $unique_name, 30 | record => $record, 31 | class => 'hidden-prop-'.$self->prop.' function-'.$self->function->name, 32 | value => $self->value, 33 | type => 'hidden') 34 | 35 | ); 36 | 37 | outs_raw( $self->field->render_input ); 38 | 39 | } 40 | __PACKAGE__->meta->make_immutable; 41 | no Any::Moose; 42 | 43 | 1; 44 | 45 | -------------------------------------------------------------------------------- /lib/Prophet/Server/ViewHelpers/ParamFromFunction.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Server::ViewHelpers::ParamFromFunction; 2 | 3 | use Template::Declare::Tags; 4 | 5 | BEGIN { delete ${__PACKAGE__."::"}{meta}; 6 | delete ${__PACKAGE__."::"}{with}; 7 | } 8 | 9 | use Any::Moose; 10 | 11 | use Any::Moose 'Util::TypeConstraints'; 12 | 13 | 14 | =head1 NAME 15 | 16 | =head1 METHODS 17 | 18 | =head1 DESCRIPTION 19 | 20 | =cut 21 | 22 | 23 | has function => ( 24 | isa => 'Prophet::Server::ViewHelpers::Function', 25 | is => 'ro' 26 | ); 27 | has name => ( isa => 'Str', is => 'rw' ); 28 | has prop => ( isa => 'Str', is => 'ro' ); 29 | has from_function => ( isa => 'Prophet::Server::ViewHelpers::Function', is => 'rw' ); 30 | has from_result => ( isa => 'Str', is => 'rw' ); 31 | has field => ( isa => 'Prophet::Web::Field', is => 'rw' ); 32 | 33 | 34 | sub render { 35 | my $self = shift; 36 | 37 | my $unique_name = $self->_generate_name(); 38 | 39 | my $record = $self->function->record; 40 | 41 | my $value = "function-".$self->from_function->name."|result-".$self->from_result; 42 | 43 | $self->field( Prophet::Web::Field->new( 44 | name => $unique_name, 45 | type => 'hidden', 46 | record => $record, 47 | value => $value 48 | 49 | )); 50 | 51 | outs_raw( $self->field->render_input ); 52 | } 53 | 54 | 55 | 56 | 57 | sub _generate_name { 58 | my $self = shift; 59 | return "prophet-fill-function-" 60 | . $self->function->name 61 | . "-prop-" 62 | . $self->prop; 63 | } 64 | 65 | =head1 METHODS 66 | 67 | =cut 68 | 69 | 70 | 71 | 72 | __PACKAGE__->meta->make_immutable; 73 | no Any::Moose; 74 | 75 | 1; 76 | 77 | -------------------------------------------------------------------------------- /lib/Prophet/Server/ViewHelpers/Widget.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Server::ViewHelpers::Widget; 2 | 3 | use Template::Declare::Tags; 4 | 5 | BEGIN { delete ${__PACKAGE__."::"}{meta}; 6 | delete ${__PACKAGE__."::"}{with}; 7 | } 8 | 9 | use Any::Moose; 10 | 11 | =head1 NAME 12 | 13 | =head1 METHODS 14 | 15 | =head1 DESCRIPTION 16 | 17 | =cut 18 | 19 | has function => ( 20 | isa => 'Prophet::Server::ViewHelpers::Function', 21 | is => 'ro' 22 | ); 23 | 24 | has name => ( isa => 'Str', is => 'rw' ); 25 | 26 | has prop => ( isa => 'Str', is => 'ro' ); 27 | 28 | has field => ( isa => 'Prophet::Web::Field', is => 'rw'); 29 | 30 | has type => ( isa => 'Str|Undef', is => 'rw'); 31 | 32 | has autocomplete => (isa => 'Bool', is => 'rw', default => 1); 33 | 34 | has default => ( isa => 'Str|Undef', is => 'rw'); 35 | 36 | sub render { 37 | my $self = shift; 38 | 39 | my $unique_name = $self->_generate_name(); 40 | 41 | my $record = $self->function->record; 42 | 43 | my $value; 44 | 45 | if (defined $self->default) { 46 | $value = $self->default; 47 | } elsif ( $self->function->action eq 'create' ) { 48 | if ( my $method = $self->function->record->can( 'default_prop_' . $self->prop ) ) { 49 | $value = $method->( $self->function->record ); 50 | } else { 51 | $value = ''; 52 | } 53 | } elsif ( $self->function->action eq 'update' && $self->function->record->loaded ) { 54 | $value = $self->function->record->prop( $self->prop ) || ''; 55 | } else { 56 | $value = ''; 57 | } 58 | 59 | $self->field( Prophet::Web::Field->new( 60 | name => $unique_name, 61 | id => $unique_name, 62 | record => $record, 63 | label => $self->prop, 64 | class => 'prop-'.$self->prop.' function-'.$self->function->name, 65 | value => $value, 66 | ($self->type ? ( type => $self->type) : ()) 67 | 68 | )); 69 | 70 | my $orig = Prophet::Web::Field->new( 71 | name => "original-value-". $unique_name, 72 | value => $value, 73 | type => 'hidden' 74 | ); 75 | 76 | outs_raw( $self->field->render ); 77 | outs_raw( $orig->render_input ); 78 | if ($self->autocomplete) { 79 | $self->_render_autocompleter(); 80 | } 81 | 82 | } 83 | 84 | sub _render_autocompleter { 85 | my $self = shift; 86 | my $record = $self->function->record(); 87 | outs_raw(' '); 95 | } 96 | 97 | sub _generate_name { 98 | my $self = shift; 99 | return 100 | "prophet-field-function-" 101 | . $self->function->name 102 | . "-prop-" 103 | . $self->prop; 104 | } 105 | 106 | =head1 METHODS 107 | 108 | =cut 109 | 110 | __PACKAGE__->meta->make_immutable; 111 | no Any::Moose; 112 | 113 | 1; 114 | 115 | -------------------------------------------------------------------------------- /lib/Prophet/Test/Editor.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Test::Editor; 2 | use strict; 3 | use warnings; 4 | 5 | use Prophet::Util; 6 | use Params::Validate; 7 | use File::Spec; 8 | 9 | =head1 METHODS 10 | 11 | =head2 edit( tmpl_files => $tmpl_files, edit_callback => sub {}, verify_callback => sub {} ) 12 | 13 | Expects @ARGV to contain at least an option and a file to be edited. It 14 | can also contain a replica uuid, a ticket uuid, and a status file. The last 15 | item must always be the file to be edited. The others, if they appear, must 16 | be in that order after the option. The status file must contain the 17 | string 'status' in its filename. 18 | 19 | edit_callback is called on each line of the file being edited. It should make 20 | any edits to the lines it receives and then print what it wants to be saved to 21 | the file. 22 | 23 | verify_callback is called after editing is done. If you need to write 24 | whether the template was correct to a status file, for example, this 25 | should be done here. 26 | 27 | =cut 28 | 29 | sub edit { 30 | my %args = @_; 31 | validate( @_, { edit_callback => 1, 32 | verify_callback => 1, 33 | tmpl_files => 1, 34 | } 35 | ); 36 | 37 | my $option = shift @ARGV; 38 | my $tmpl_file = $args{tmpl_files}->{$option}; 39 | 40 | my @valid_template = Prophet::Util->slurp("t/data/$tmpl_file"); 41 | chomp @valid_template; 42 | 43 | my $status_file = $ARGV[-2] =~ /status/ ? delete $ARGV[-2] : undef; 44 | # a bit of a hack to dermine whether the last arg is a filename 45 | my $replica_uuid = File::Spec->file_name_is_absolute($ARGV[0]) ? undef : shift @ARGV; 46 | my $ticket_uuid = File::Spec->file_name_is_absolute($ARGV[0]) ? undef : shift @ARGV; 47 | 48 | my @template = (); 49 | while (<>) { 50 | chomp( my $line = $_ ); 51 | push @template, $line; 52 | 53 | $args{edit_callback}( 54 | option => $option, template => \@template, 55 | valid_template => \@valid_template, 56 | replica_uuid => $replica_uuid, 57 | ticket_uuid => $ticket_uuid, 58 | ); 59 | } 60 | 61 | $args{verify_callback}( template => \@template, 62 | valid_template => \@valid_template, status_file => $status_file ); 63 | } 64 | 65 | =head2 check_template_by_line($template, $valid_template, $errors) 66 | 67 | $template is a reference to an array containing the template to check, 68 | split into lines. $valid_template is the same for the template to 69 | check against. Lines in these arrays should not have trailing newlines. 70 | $errors is a reference to an array where error messages will be stored. 71 | 72 | Lines in $valid_template should consist of either plain strings, or strings 73 | beginning with 'qr/' (to delimit a regexp object). 74 | 75 | Returns true if the templates match and false otherwise. 76 | 77 | =cut 78 | 79 | sub check_template_by_line { 80 | my @template = @{ shift @_ }; 81 | my @valid_template = @{ shift @_ }; 82 | my $replica_uuid = shift; 83 | my $ticket_uuid = shift; 84 | my $errors = shift; 85 | 86 | for my $valid_line (@valid_template) { 87 | my $line = shift @template; 88 | 89 | push @$errors, "got nothing, expected [$valid_line]" if !defined($line); 90 | 91 | push @$errors, "[$line] doesn't match [$valid_line]" 92 | if ($valid_line =~ /^qr\//) ? $line !~ eval($valid_line) 93 | : $line eq $valid_line; 94 | } 95 | 96 | return !(@$errors == 0); 97 | } 98 | 99 | 1; 100 | -------------------------------------------------------------------------------- /lib/Prophet/UUIDGenerator.pm: -------------------------------------------------------------------------------- 1 | package Prophet::UUIDGenerator; 2 | use Any::Moose; 3 | use MIME::Base64::URLSafe; 4 | 5 | =head1 NAME 6 | 7 | Prophet::UUIDGenerator 8 | 9 | =head1 DESCRIPTION 10 | 11 | Creates UUIDs using L. Initially, it created v1 and v3 12 | UUIDs; the new UUID scheme creates v4 and v5 UUIDs, instead. 13 | 14 | =head1 METHODS 15 | 16 | =head2 uuid_scheme 17 | 18 | Gets or sets the UUID scheme; if 1, then creates v1 and v3 UUIDs (for 19 | backward compatability with earlier versions of Prophet). If 2, it 20 | creates v4 and v5 UUIDs. 21 | 22 | =cut 23 | 24 | use UUID::Tiny ':std'; 25 | 26 | # uuid_scheme: 1 - v1 and v3 uuids. 27 | # 2 - v4 and v5 uuids. 28 | 29 | has uuid_scheme => ( 30 | isa => 'Int', 31 | is => 'rw' 32 | ); 33 | 34 | =head2 create_str 35 | 36 | Creates and returns v1 or v4 UUIDs, depending on L. 37 | 38 | =cut 39 | 40 | sub create_str { 41 | my $self = shift; 42 | if ($self->uuid_scheme == 1 ){ 43 | return create_uuid_as_string(UUID_V1); 44 | } elsif ($self->uuid_scheme == 2) { 45 | return create_uuid_as_string(UUID_V4); 46 | } 47 | } 48 | 49 | =head2 create_string_from_url URL 50 | 51 | Creates and returns v3 or v5 UUIDs for the given C, depending on 52 | L. 53 | 54 | =cut 55 | 56 | sub create_string_from_url { 57 | my $self = shift; 58 | my $url = shift; 59 | local $!; 60 | if ($self->uuid_scheme == 1 ){ 61 | # Yes, DNS, not URL. We screwed up when we first defined it 62 | # and it can't be safely changed once defined. 63 | create_uuid_as_string(UUID_V3, UUID_NS_DNS, $url); 64 | } elsif ($self->uuid_scheme == 2) { 65 | create_uuid_as_string(UUID_V5, UUID_NS_URL, $url); 66 | } 67 | } 68 | 69 | =head2 from_string 70 | 71 | =cut 72 | 73 | sub from_string { 74 | my $self = shift; 75 | my $str = shift; 76 | return string_to_uuid($str); 77 | } 78 | 79 | =head2 to_string 80 | 81 | =cut 82 | 83 | 84 | sub to_string { 85 | my $self = shift; 86 | my $uuid = shift; 87 | return uuid_to_string($uuid); 88 | } 89 | 90 | =head2 from_safe_b64 91 | 92 | =cut 93 | 94 | sub from_safe_b64 { 95 | my $self = shift; 96 | my $uuid = shift; 97 | return urlsafe_b64decode($uuid); 98 | } 99 | 100 | =head2 to_safe_b64 101 | 102 | =cut 103 | 104 | sub to_safe_b64 { 105 | my $self = shift; 106 | my $uuid = shift; 107 | return urlsafe_b64encode($self->from_string($uuid)); 108 | } 109 | 110 | =head2 version 111 | 112 | =cut 113 | 114 | sub version { 115 | my $self = shift; 116 | my $uuid = shift; 117 | return version_of_uuid($uuid); 118 | } 119 | 120 | =head2 set_uuid_scheme 121 | 122 | =cut 123 | 124 | sub set_uuid_scheme { 125 | my $self = shift; 126 | my $uuid = shift; 127 | 128 | if ( $self->version($uuid) <= 3 ) { 129 | $self->uuid_scheme(1); 130 | } else { 131 | $self->uuid_scheme(2); 132 | } 133 | } 134 | 135 | __PACKAGE__->meta->make_immutable; 136 | no Any::Moose; 137 | 138 | 1; 139 | 140 | -------------------------------------------------------------------------------- /lib/Prophet/Util.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Util; 2 | use strict; 3 | use File::Basename; 4 | use File::Spec; 5 | use File::Path; 6 | use Params::Validate; 7 | use Cwd; 8 | 9 | =head2 updir PATH, DEPTH 10 | 11 | Strips off the filename in the given path and returns the absolute 12 | path of the remaining directory. 13 | 14 | Default depth is 1. 15 | If depth are great than 1, will go up more according to the depth value. 16 | 17 | =cut 18 | 19 | sub updir { 20 | my $self = shift; 21 | my ( $path, $depth ) = validate_pos( @_, 1, { default => 1 } ); 22 | die "depth must be positive" unless $depth > 0; 23 | 24 | my ($file, $dir, undef) = fileparse(File::Spec->rel2abs($path)); 25 | 26 | $depth-- if $file; # we stripped the file part 27 | 28 | if ($depth) { 29 | $dir = File::Spec->catdir( $dir, ( File::Spec->updir ) x $depth ); 30 | } 31 | 32 | # if $dir doesn't exists in file system, abs_path will return empty 33 | return Cwd::abs_path($dir) || $dir; 34 | } 35 | 36 | =head2 slurp FILENAME 37 | 38 | Reads in the entire file whose absolute path is given by FILENAME and 39 | returns its contents, either in a scalar or in an array of lines, 40 | depending on the context. 41 | 42 | =cut 43 | 44 | sub slurp { 45 | my $self = shift; 46 | my $abspath = shift; 47 | open (my $fh, "<", "$abspath") || die "$abspath: $!"; 48 | 49 | my @lines = <$fh>; 50 | close $fh; 51 | 52 | return wantarray ? @lines : join('',@lines); 53 | } 54 | 55 | =head2 instantiate_record class => 'record-class-name', uuid => 'record-uuid', app_handle => $self->app_handle 56 | 57 | Takes the name of a record class (must subclass L), a uuid, 58 | and an application handle and returns a new instantiated record object 59 | of the given class. 60 | 61 | =cut 62 | 63 | sub instantiate_record { 64 | my $self = shift; 65 | my %args = validate(@_, { 66 | class => 1, 67 | uuid => 1, 68 | app_handle => 1 69 | 70 | }); 71 | die $args{class} ." is not a valid class " unless (UNIVERSAL::isa($args{class}, 'Prophet::Record')); 72 | my $object = $args{class}->new( uuid => $args{uuid}, app_handle => $args{app_handle}); 73 | return $object; 74 | } 75 | 76 | =head2 escape_utf8 REF 77 | 78 | Given a reference to a scalar, escapes special characters (currently just &, <, 79 | >, (, ), ", and ') for use in HTML and XML. 80 | 81 | Not an object routine (call as Prophet::Util::escape_utf8( \$scalar) ). 82 | 83 | =cut 84 | 85 | sub escape_utf8 { 86 | my $ref = shift; 87 | no warnings 'uninitialized'; 88 | $$ref =~ s/&/&/g; 89 | $$ref =~ s//>/g; 91 | $$ref =~ s/\(/(/g; 92 | $$ref =~ s/\)/)/g; 93 | $$ref =~ s/"/"/g; 94 | $$ref =~ s/'/'/g; 95 | } 96 | 97 | 98 | sub write_file { 99 | my $self = shift; 100 | my %args = (@_); #validate is too heavy to be called here 101 | # my %args = validate( @_, { file => 1, content => 1 } ); 102 | 103 | my ( undef, $parent, $filename ) = File::Spec->splitpath($args{file}); 104 | unless ( -d $parent ) { 105 | eval { mkpath( [$parent] ) }; 106 | if ( my $msg = $@ ) { 107 | die "Failed to create directory " . $parent . " - $msg"; 108 | } 109 | } 110 | 111 | open( my $fh, ">", $args{file} ) || die $!; 112 | print $fh scalar( $args{'content'} ) 113 | ; # can't do "||" as we die if we print 0" || die "Could not write to " . $args{'path'} . " " . $!; 114 | close $fh || die $!; 115 | } 116 | 117 | sub hashed_dir_name { 118 | my $hash = shift; 119 | 120 | return ( substr( $hash, 0, 1 ), substr( $hash, 1, 1 ), $hash ); 121 | } 122 | 123 | sub catfile { 124 | my $self = shift; 125 | 126 | # File::Spec::catfile is more correct, but 127 | # eats over 10% of prophet app runtime, 128 | # which isn't acceptable. 129 | return join('/',@_); 130 | 131 | } 132 | 133 | 1; 134 | -------------------------------------------------------------------------------- /lib/Prophet/Web/Field.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Web::Field; 2 | use Any::Moose; 3 | 4 | has name => ( isa => 'Str', is => 'rw' ); 5 | has record => ( isa => 'Prophet::Record', is => 'rw' ); 6 | has prop => ( isa => 'Str', is => 'rw' ); 7 | has value => ( isa => 'Str', is => 'rw' ); 8 | has label => ( isa => 'Str', is => 'rw', default => sub {''}); 9 | has id => ( isa => 'Str|Undef', is => 'rw' ); 10 | has class => ( isa => 'Str|Undef', is => 'rw' ); 11 | has value => ( isa => 'Str|Undef', is => 'rw' ); 12 | has type => ( isa => 'Str|Undef', is => 'rw', default => 'text'); 13 | 14 | 15 | 16 | sub _render_attr { 17 | my $self = shift; 18 | my $attr = shift; 19 | my $value = $self->$attr() || return ''; 20 | Prophet::Util::escape_utf8(\$value); 21 | return $attr . '="' . $value . '"'; 22 | } 23 | 24 | sub render_name { 25 | my $self = shift; 26 | $self->_render_attr('name'); 27 | 28 | } 29 | 30 | sub render_id { 31 | my $self = shift; 32 | $self->_render_attr('id'); 33 | } 34 | 35 | sub render_class { 36 | my $self = shift; 37 | $self->_render_attr('class'); 38 | } 39 | 40 | sub render_value { 41 | my $self = shift; 42 | $self->_render_attr('value'); 43 | } 44 | 45 | sub render { 46 | my $self = shift; 47 | 48 | my $output = <render_name]} @{[$self->render_class]}>@{[$self->label]} 50 | @{[$self->render_input]} 51 | 52 | 53 | EOF 54 | 55 | return $output; 56 | 57 | } 58 | 59 | sub render_input { 60 | my $self = shift; 61 | 62 | if ($self->type eq 'textarea') { 63 | my $value = $self->value() || ''; 64 | Prophet::Util::escape_utf8(\$value); 65 | 66 | return <render_name]} @{[$self->render_id]} @{[$self->render_class]} >@{[$value]} 68 | EOF 69 | } else { 70 | 71 | return <render_name]} @{[$self->render_id]} @{[$self->render_class]} @{[$self->render_value]} /> 73 | EOF 74 | 75 | } 76 | 77 | } 78 | 79 | 80 | 81 | __PACKAGE__->meta->make_immutable; 82 | no Any::Moose; 83 | 84 | 1; 85 | -------------------------------------------------------------------------------- /lib/Prophet/Web/FunctionResult.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Web::FunctionResult; 2 | use Any::Moose; 3 | 4 | =head1 NAME 5 | 6 | =head1 METHODS 7 | 8 | =head1 DESCRIPTION 9 | 10 | =cut 11 | 12 | =head1 METHODS 13 | 14 | =cut 15 | 16 | has class => ( isa => 'Str', is => 'rw'); 17 | has function_name => ( isa => 'Str', is => 'rw'); 18 | has record_uuid => (isa => 'Str|Undef', is => 'rw'); 19 | has success => (isa => 'Bool', is => 'rw'); 20 | has message => (isa => 'Str', is => 'rw'); 21 | 22 | has result => ( 23 | is => 'rw', 24 | isa => 'HashRef', 25 | default => sub { {} }, 26 | ); 27 | 28 | sub exists { exists $_[0]->result->{$_[1]} } 29 | sub items { keys %{ $_[0]->result } } 30 | sub get { $_[0]->result>{$_[1]} } 31 | sub set { $_[0]->result->{$_[1]} = $_[2] } 32 | 33 | __PACKAGE__->meta->make_immutable; 34 | no Any::Moose; 35 | 36 | 1; 37 | 38 | -------------------------------------------------------------------------------- /lib/Prophet/Web/Result.pm: -------------------------------------------------------------------------------- 1 | package Prophet::Web::Result; 2 | use Any::Moose; 3 | 4 | use Prophet::Web::FunctionResult; 5 | 6 | =head1 NAME 7 | 8 | Prophet::Web::Result 9 | 10 | =head1 METHODS 11 | 12 | =head1 DESCRIPTION 13 | 14 | =cut 15 | 16 | =head1 METHODS 17 | 18 | =cut 19 | 20 | has success => ( isa => 'Bool', is => 'rw'); 21 | has message => ( isa => 'Str', is => 'rw'); 22 | has functions => ( 23 | is => 'rw', 24 | isa => 'HashRef', 25 | default => sub { {} }, 26 | ); 27 | 28 | sub get { $_[0]->functions->{$_[1]} } 29 | sub set { $_[0]->functions->{$_[1]} = $_[2] } 30 | sub exists { exists $_[0]->functions->{$_[1]} } 31 | sub items { keys %{ $_[0]->functions } } 32 | 33 | __PACKAGE__->meta->make_immutable; 34 | no Any::Moose; 35 | 36 | 1; 37 | 38 | -------------------------------------------------------------------------------- /share/web/static/jquery/css/indicator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestpractical/prophet/600ec2f2525ac8ebd2687cb2348296dab208cf14/share/web/static/jquery/css/indicator.gif -------------------------------------------------------------------------------- /share/web/static/jquery/css/jquery.autocomplete.css: -------------------------------------------------------------------------------- 1 | .ac_results { 2 | border-right: 1px solid #666; 3 | border-bottom: 1px solid #666; 4 | background-color: white; 5 | overflow: hidden; 6 | z-index: 99999; 7 | } 8 | 9 | .ac_results ul { 10 | width: 100%; 11 | list-style-position: outside; 12 | list-style: none; 13 | padding: 0; 14 | margin: 0; 15 | } 16 | 17 | .ac_results li { 18 | /*margin: 0px; 19 | padding: 2px 5px; */ 20 | padding: 0.5em; 21 | cursor: default; 22 | display: block; 23 | /* 24 | if width will be 100% horizontal scrollbar will apear 25 | when scroll mode will be used 26 | */ 27 | /*width: 100%;*/ 28 | font: menu; 29 | font-size: 0.7em; 30 | /* 31 | it is very important, if line-height not setted or setted 32 | in relative units scroll will be broken in firefox 33 | */ 34 | overflow: hidden; 35 | } 36 | 37 | .ac_loading { 38 | /* background: white url('indicator.gif') right center no-repeat; */ 39 | } 40 | 41 | .ac_odd { 42 | background-color: #efefef; 43 | } 44 | 45 | .ac_even { 46 | background-color: #fefefe; 47 | } 48 | 49 | .ac_over { 50 | background-color: #ccc; 51 | color: #000; 52 | } 53 | -------------------------------------------------------------------------------- /share/web/static/jquery/css/superfish-navbar.css: -------------------------------------------------------------------------------- 1 | 2 | /*** adding the class sf-navbar in addition to sf-menu creates an all-horizontal nav-bar menu ***/ 3 | .sf-navbar { 4 | background: #BDD2FF; 5 | height: 2.5em; 6 | padding-bottom: 2.5em; 7 | position: relative; 8 | } 9 | .sf-navbar li { 10 | background: #AABDE6; 11 | position: static; 12 | } 13 | .sf-navbar a { 14 | border-top: none; 15 | } 16 | .sf-navbar li ul { 17 | width: 44em; /*IE6 soils itself without this*/ 18 | } 19 | .sf-navbar li li { 20 | background: #BDD2FF; 21 | position: relative; 22 | } 23 | .sf-navbar li li ul { 24 | width: 13em; 25 | } 26 | .sf-navbar li li li { 27 | width: 100%; 28 | } 29 | .sf-navbar ul li { 30 | width: auto; 31 | float: left; 32 | } 33 | .sf-navbar a, .sf-navbar a:visited { 34 | border: none; 35 | } 36 | .sf-navbar li.current { 37 | background: #BDD2FF; 38 | } 39 | .sf-navbar li:hover, 40 | .sf-navbar li.sfHover, 41 | .sf-navbar li li.current, 42 | .sf-navbar a:focus, .sf-navbar a:hover, .sf-navbar a:active { 43 | background: #BDD2FF; 44 | } 45 | .sf-navbar ul li:hover, 46 | .sf-navbar ul li.sfHover, 47 | ul.sf-navbar ul li:hover li, 48 | ul.sf-navbar ul li.sfHover li, 49 | .sf-navbar ul a:focus, .sf-navbar ul a:hover, .sf-navbar ul a:active { 50 | background: #D1DFFF; 51 | } 52 | ul.sf-navbar li li li:hover, 53 | ul.sf-navbar li li li.sfHover, 54 | .sf-navbar li li.current li.current, 55 | .sf-navbar ul li li a:focus, .sf-navbar ul li li a:hover, .sf-navbar ul li li a:active { 56 | background: #E6EEFF; 57 | } 58 | ul.sf-navbar .current ul, 59 | ul.sf-navbar ul li:hover ul, 60 | ul.sf-navbar ul li.sfHover ul { 61 | left: 0; 62 | top: 2.5em; /* match top ul list item height */ 63 | } 64 | ul.sf-navbar .current ul ul { 65 | top: -999em; 66 | } 67 | 68 | .sf-navbar li li.current > a { 69 | font-weight: bold; 70 | } 71 | 72 | /*** point all arrows down ***/ 73 | /* point right for anchors in subs */ 74 | .sf-navbar ul .sf-sub-indicator { background-position: -10px -100px; } 75 | .sf-navbar ul a > .sf-sub-indicator { background-position: 0 -100px; } 76 | /* apply hovers to modern browsers */ 77 | .sf-navbar ul a:focus > .sf-sub-indicator, 78 | .sf-navbar ul a:hover > .sf-sub-indicator, 79 | .sf-navbar ul a:active > .sf-sub-indicator, 80 | .sf-navbar ul li:hover > a > .sf-sub-indicator, 81 | .sf-navbar ul li.sfHover > a > .sf-sub-indicator { 82 | background-position: -10px -100px; /* arrow hovers for modern browsers*/ 83 | } 84 | 85 | /*** remove shadow on first submenu ***/ 86 | .sf-navbar > li > ul { 87 | background: transparent; 88 | padding: 0; 89 | -moz-border-radius-bottomleft: 0; 90 | -moz-border-radius-topright: 0; 91 | -webkit-border-top-right-radius: 0; 92 | -webkit-border-bottom-left-radius: 0; 93 | } -------------------------------------------------------------------------------- /share/web/static/jquery/css/superfish-vertical.css: -------------------------------------------------------------------------------- 1 | /*** adding sf-vertical in addition to sf-menu creates a vertical menu ***/ 2 | .sf-vertical, .sf-vertical li { 3 | width: 10em; 4 | } 5 | /* this lacks ul at the start of the selector, so the styles from the main CSS file override it where needed */ 6 | .sf-vertical li:hover ul, 7 | .sf-vertical li.sfHover ul { 8 | left: 10em; /* match ul width */ 9 | top: 0; 10 | } 11 | 12 | /*** alter arrow directions ***/ 13 | .sf-vertical .sf-sub-indicator { background-position: -10px 0; } /* IE6 gets solid image only */ 14 | .sf-vertical a > .sf-sub-indicator { background-position: 0 0; } /* use translucent arrow for modern browsers*/ 15 | 16 | /* hover arrow direction for modern browsers*/ 17 | .sf-vertical a:focus > .sf-sub-indicator, 18 | .sf-vertical a:hover > .sf-sub-indicator, 19 | .sf-vertical a:active > .sf-sub-indicator, 20 | .sf-vertical li:hover > a > .sf-sub-indicator, 21 | .sf-vertical li.sfHover > a > .sf-sub-indicator { 22 | background-position: -10px 0; /* arrow hovers for modern browsers*/ 23 | } -------------------------------------------------------------------------------- /share/web/static/jquery/css/superfish.css: -------------------------------------------------------------------------------- 1 | 2 | /*** ESSENTIAL STYLES ***/ 3 | .sf-menu, .sf-menu * { 4 | margin: 0; 5 | padding: 0; 6 | list-style: none; 7 | } 8 | .sf-menu { 9 | line-height: 1.0; 10 | } 11 | .sf-menu ul { 12 | position: absolute; 13 | top: -999em; 14 | width: 10em; /* left offset of submenus need to match (see below) */ 15 | } 16 | .sf-menu ul li { 17 | width: 100%; 18 | } 19 | .sf-menu li:hover { 20 | visibility: inherit; /* fixes IE7 'sticky bug' */ 21 | } 22 | .sf-menu li { 23 | float: left; 24 | position: relative; 25 | } 26 | .sf-menu a { 27 | display: block; 28 | position: relative; 29 | } 30 | .sf-menu li:hover ul, 31 | .sf-menu li.sfHover ul { 32 | left: 0; 33 | top: 2.5em; /* match top ul list item height */ 34 | z-index: 99; 35 | } 36 | ul.sf-menu li:hover li ul, 37 | ul.sf-menu li.sfHover li ul { 38 | top: -999em; 39 | } 40 | ul.sf-menu li li:hover ul, 41 | ul.sf-menu li li.sfHover ul { 42 | left: 10em; /* match ul width */ 43 | top: 0; 44 | } 45 | ul.sf-menu li li:hover li ul, 46 | ul.sf-menu li li.sfHover li ul { 47 | top: -999em; 48 | } 49 | ul.sf-menu li li li:hover ul, 50 | ul.sf-menu li li li.sfHover ul { 51 | left: 10em; /* match ul width */ 52 | top: 0; 53 | } 54 | 55 | /*** DEMO SKIN ***/ 56 | .sf-menu { 57 | float: left; 58 | margin-bottom: 1em; 59 | } 60 | .sf-menu a { 61 | border-left: 1px solid #fff; 62 | border-top: 1px solid #CFDEFF; 63 | padding: .75em 1em; 64 | text-decoration:none; 65 | } 66 | .sf-menu a, .sf-menu a:visited { /* visited pseudo selector so IE6 applies text colour*/ 67 | color: #13a; 68 | } 69 | .sf-menu li { 70 | background: #BDD2FF; 71 | } 72 | .sf-menu li li { 73 | background: #AABDE6; 74 | } 75 | .sf-menu li li li { 76 | background: #9AAEDB; 77 | } 78 | .sf-menu li:hover, .sf-menu li.sfHover, 79 | .sf-menu a:focus, .sf-menu a:hover, .sf-menu a:active { 80 | background: #CFDEFF; 81 | outline: 0; 82 | } 83 | 84 | /*** arrows **/ 85 | .sf-menu a.sf-with-ul { 86 | padding-right: 2.25em; 87 | min-width: 1px; /* trigger IE7 hasLayout so spans position accurately */ 88 | } 89 | .sf-sub-indicator { 90 | position: absolute; 91 | display: block; 92 | right: .75em; 93 | top: 1.05em; /* IE6 only */ 94 | width: 10px; 95 | height: 10px; 96 | text-indent: -999em; 97 | overflow: hidden; 98 | background: url('../images/arrows-ffffff.png') no-repeat -10px -100px; /* 8-bit indexed alpha png. IE6 gets solid image only */ 99 | } 100 | a > .sf-sub-indicator { /* give all except IE6 the correct values */ 101 | top: .8em; 102 | background-position: 0 -100px; /* use translucent arrow for modern browsers*/ 103 | } 104 | /* apply hovers to modern browsers */ 105 | a:focus > .sf-sub-indicator, 106 | a:hover > .sf-sub-indicator, 107 | a:active > .sf-sub-indicator, 108 | li:hover > a > .sf-sub-indicator, 109 | li.sfHover > a > .sf-sub-indicator { 110 | background-position: -10px -100px; /* arrow hovers for modern browsers*/ 111 | } 112 | 113 | /* point right for anchors in subs */ 114 | .sf-menu ul .sf-sub-indicator { background-position: -10px 0; } 115 | .sf-menu ul a > .sf-sub-indicator { background-position: 0 0; } 116 | /* apply hovers to modern browsers */ 117 | .sf-menu ul a:focus > .sf-sub-indicator, 118 | .sf-menu ul a:hover > .sf-sub-indicator, 119 | .sf-menu ul a:active > .sf-sub-indicator, 120 | .sf-menu ul li:hover > a > .sf-sub-indicator, 121 | .sf-menu ul li.sfHover > a > .sf-sub-indicator { 122 | background-position: -10px 0; /* arrow hovers for modern browsers*/ 123 | } 124 | 125 | /*** shadows for all but IE6 ***/ 126 | .sf-shadow ul { 127 | background: url('../images/shadow.png') no-repeat bottom right; 128 | padding: 0 8px 9px 0; 129 | -moz-border-radius-bottomleft: 17px; 130 | -moz-border-radius-topright: 17px; 131 | -webkit-border-top-right-radius: 17px; 132 | -webkit-border-bottom-left-radius: 17px; 133 | } 134 | .sf-shadow ul.sf-shadow-off { 135 | background: transparent; 136 | } 137 | -------------------------------------------------------------------------------- /share/web/static/jquery/css/tablesorter/asc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestpractical/prophet/600ec2f2525ac8ebd2687cb2348296dab208cf14/share/web/static/jquery/css/tablesorter/asc.gif -------------------------------------------------------------------------------- /share/web/static/jquery/css/tablesorter/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestpractical/prophet/600ec2f2525ac8ebd2687cb2348296dab208cf14/share/web/static/jquery/css/tablesorter/bg.gif -------------------------------------------------------------------------------- /share/web/static/jquery/css/tablesorter/desc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestpractical/prophet/600ec2f2525ac8ebd2687cb2348296dab208cf14/share/web/static/jquery/css/tablesorter/desc.gif -------------------------------------------------------------------------------- /share/web/static/jquery/css/tablesorter/style.css: -------------------------------------------------------------------------------- 1 | /* tables */ 2 | table.tablesorter { 3 | font-family:arial; 4 | background-color: #CDCDCD; 5 | margin:10px 0pt 15px; 6 | font-size: 8pt; 7 | width: 100%; 8 | text-align: left; 9 | } 10 | table.tablesorter thead tr th, table.tablesorter tfoot tr th { 11 | background-color: #e6EEEE; 12 | border: 1px solid #FFF; 13 | font-size: 8pt; 14 | padding: 4px; 15 | } 16 | table.tablesorter thead tr .header { 17 | background-image: url(bg.gif); 18 | background-repeat: no-repeat; 19 | background-position: center right; 20 | cursor: pointer; 21 | } 22 | table.tablesorter tbody td { 23 | color: #3D3D3D; 24 | padding: 4px; 25 | background-color: #FFF; 26 | vertical-align: top; 27 | } 28 | table.tablesorter tbody tr.odd td { 29 | background-color:#F0F0F6; 30 | } 31 | table.tablesorter thead tr .headerSortUp { 32 | background-image: url(asc.gif); 33 | } 34 | table.tablesorter thead tr .headerSortDown { 35 | background-image: url(desc.gif); 36 | } 37 | table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp { 38 | background-color: #8dbdd8; 39 | } 40 | -------------------------------------------------------------------------------- /share/web/static/jquery/images/arrows-cccccc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestpractical/prophet/600ec2f2525ac8ebd2687cb2348296dab208cf14/share/web/static/jquery/images/arrows-cccccc.png -------------------------------------------------------------------------------- /share/web/static/jquery/images/arrows-ffffff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestpractical/prophet/600ec2f2525ac8ebd2687cb2348296dab208cf14/share/web/static/jquery/images/arrows-ffffff.png -------------------------------------------------------------------------------- /share/web/static/jquery/images/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bestpractical/prophet/600ec2f2525ac8ebd2687cb2348296dab208cf14/share/web/static/jquery/images/shadow.png -------------------------------------------------------------------------------- /share/web/static/jquery/js/hoverIntent.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | /* hoverIntent by Brian Cherne */ 3 | $.fn.hoverIntent = function(f,g) { 4 | // default configuration options 5 | var cfg = { 6 | sensitivity: 7, 7 | interval: 100, 8 | timeout: 0 9 | }; 10 | // override configuration options with user supplied object 11 | cfg = $.extend(cfg, g ? { over: f, out: g } : f ); 12 | 13 | // instantiate variables 14 | // cX, cY = current X and Y position of mouse, updated by mousemove event 15 | // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval 16 | var cX, cY, pX, pY; 17 | 18 | // A private function for getting mouse position 19 | var track = function(ev) { 20 | cX = ev.pageX; 21 | cY = ev.pageY; 22 | }; 23 | 24 | // A private function for comparing current and previous mouse position 25 | var compare = function(ev,ob) { 26 | ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); 27 | // compare mouse positions to see if they've crossed the threshold 28 | if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) { 29 | $(ob).unbind("mousemove",track); 30 | // set hoverIntent state to true (so mouseOut can be called) 31 | ob.hoverIntent_s = 1; 32 | return cfg.over.apply(ob,[ev]); 33 | } else { 34 | // set previous coordinates for next time 35 | pX = cX; pY = cY; 36 | // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs) 37 | ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval ); 38 | } 39 | }; 40 | 41 | // A private function for delaying the mouseOut function 42 | var delay = function(ev,ob) { 43 | ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); 44 | ob.hoverIntent_s = 0; 45 | return cfg.out.apply(ob,[ev]); 46 | }; 47 | 48 | // A private function for handling mouse 'hovering' 49 | var handleHover = function(e) { 50 | // next three lines copied from jQuery.hover, ignore children onMouseOver/onMouseOut 51 | var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget; 52 | while ( p && p != this ) { try { p = p.parentNode; } catch(e) { p = this; } } 53 | if ( p == this ) { return false; } 54 | 55 | // copy objects to be passed into t (required for event object to be passed in IE) 56 | var ev = jQuery.extend({},e); 57 | var ob = this; 58 | 59 | // cancel hoverIntent timer if it exists 60 | if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); } 61 | 62 | // else e.type == "onmouseover" 63 | if (e.type == "mouseover") { 64 | // set "previous" X and Y position based on initial entry point 65 | pX = ev.pageX; pY = ev.pageY; 66 | // update "current" X and Y position based on mousemove 67 | $(ob).bind("mousemove",track); 68 | // start polling interval (self-calling timeout) to compare mouse coordinates over time 69 | if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );} 70 | 71 | // else e.type == "onmouseout" 72 | } else { 73 | // unbind expensive mousemove event 74 | $(ob).unbind("mousemove",track); 75 | // if hoverIntent state is true, then call the mouseOut function after the specified delay 76 | if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );} 77 | } 78 | }; 79 | 80 | // bind the function to the two event listeners 81 | return this.mouseover(handleHover).mouseout(handleHover); 82 | }; 83 | 84 | })(jQuery); -------------------------------------------------------------------------------- /share/web/static/jquery/js/jquery.bgiframe.min.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net) 2 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 3 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 4 | * 5 | * $LastChangedDate: 2007-06-19 20:25:28 -0500 (Tue, 19 Jun 2007) $ 6 | * $Rev: 2111 $ 7 | * 8 | * Version 2.1 9 | */ 10 | (function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&parseInt($.browser.version)<=6){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='