From: <mp...@kr...> - 2010-03-08 00:06:28
|
Revision: 6346 Author: mpeters Date: 2010-03-07 19:06:20 -0500 (Sun, 07 Mar 2010) Log Message: ----------- Prevent Krang users from creating stories or categories with certain URLs. This is useful when you have another application serving the Krang published content and it needs certain URLs reserved for it's functionality. This can be set per-instance or globally. It's a space separated list of URLs that can be either relative (so that they apply to all sites in the instance) or absolute. Modified Paths: -------------- trunk/krang/conf/krang.conf.sample trunk/krang/conf/krang.conf.tmpl trunk/krang/conf/messages.conf trunk/krang/docs/changelog.pod trunk/krang/lib/Krang/CGI/Category.pm trunk/krang/lib/Krang/CGI/Story.pm trunk/krang/lib/Krang/Category.pm trunk/krang/lib/Krang/Conf.pm trunk/krang/lib/Krang/Story.pm Modified: trunk/krang/conf/krang.conf.sample =================================================================== --- trunk/krang/conf/krang.conf.sample 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/conf/krang.conf.sample 2010-03-08 00:06:20 UTC (rev 6346) @@ -230,5 +230,14 @@ # InstanceSSLCACertificateFile /path/to/krang/conf/rabbit-ca_client.crt # InstanceSSLCARevocationFile /path/to/krang/conf/rabbit-ca_client.crl + # Prevent Krang users from creating stories or categories with certain URLs. + # This is useful when you have another application serving the Krang published + # content and it needs certain URLs reserved for it's functionality. + # This can be set per-instance or globally. It's a space separated list of URLs + # that can be either relative (so that they apply to all sites in the instance) + # or absolute. + + # ReservedURLs "/api /login" + </Instance> Modified: trunk/krang/conf/krang.conf.tmpl =================================================================== --- trunk/krang/conf/krang.conf.tmpl 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/conf/krang.conf.tmpl 2010-03-08 00:06:20 UTC (rev 6346) @@ -290,5 +290,14 @@ # InstanceSSLCertificateChainFile /path/to/krang/conf/rabbit-ca.crt # InstanceSSLCACertificateFile /path/to/krang/conf/rabbit-ca_client.crt # InstanceSSLCARevocationFile /path/to/krang/conf/rabbit-ca_client.crl + + # Prevent Krang users from creating stories or categories with certain URLs. + # This is useful when you have another application serving the Krang published + # content and it needs certain URLs reserved for it's functionality. + # This can be set per-instance or globally. It's a space separated list of URLs + # that can be either relative (so that they apply to all sites in the instance) + # or absolute. + + # ReservedURLs "/api /login" </tmpl_if> </Instance> Modified: trunk/krang/conf/messages.conf =================================================================== --- trunk/krang/conf/messages.conf 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/conf/messages.conf 2010-03-08 00:06:20 UTC (rev 6346) @@ -53,6 +53,7 @@ category_has_url "Duplicate URL$s detected. Your choices generate the exact URL$s of Category $ids ($urls). Please change your settings for $attributes." category_has_url_on_add_cat "Duplicate URL detected. Adding your slug to $cat would generate the exact URL of Category $id ($url)" duplicate_url "Duplicate URL detected. Your choices for $attributes conflict with another Story's URL:" + reserved_url "We're sorry, but $reserved is a reserved URL and can't be used for stories." duplicate_urls "Duplicate URLs detected. Your choices for $attributes conflict with URLs of other Stories:" duplicate_url_on_add_cat "Duplicate URL detected. Adding Category $cat would conflict with another Story's URL:" duplicate_url_table "<center> <table border=1 style='text-align:center'> <tr> <td>Story ID</td> <td>URL (click to view)</td> </tr> $dupe_rows <tr> <td><input type='button' value='Cancel' onclick='Krang.Messages.hide($qalerts$q)'></td> <td><input type='submit' value='Delete & Replace With Your Story' onclick='if (confirm($q Are you SURE? Deleting a story also severs any links to it. $q)) { Krang.Messages.hide($qalerts$q); Krang.Form.submit($q$form$q, { rm: $qreplace_dupes$q }, { to_top: false })}'></td> </tr> </table> <br><br>" @@ -336,6 +337,7 @@ missing_dir "Directory requires a value." bad_dir "Directory name must contain only letters, numbers, underscores and dashes. No spaces or other special characters are allowed." duplicate_url "Duplicate URL detected. Category $category_id has the URL $url which is the same as your choices generate. Please change your settings for parent or directory to generate a new URL." + reserved_url "We're sorry, but $reserved is a reserved URL and can't be used for categories." story_has_category_url "Story $id has the exact URL of your new Category; it has been converted into a Category Index." uneditable_story_has_url "Story $id has the exact URL that your choices generate, and cannot be converted into a Category Index because you don't have the necessary permissions to edit it." story_had_category_url "Story $id has been turned into a category index, because it had the same URL as the just created Category $cat_id." Modified: trunk/krang/docs/changelog.pod =================================================================== --- trunk/krang/docs/changelog.pod 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/docs/changelog.pod 2010-03-08 00:06:20 UTC (rev 6346) @@ -8,6 +8,14 @@ =item * +Adding C<ReservedURLs> configuration directive. This prevents Krang +users from creating stories or categories with certain URLs. This is +useful when you have another application serving the Krang published +content and it needs certain URLs reserved for it's functionality. +This can be set per-instance or globally. + +=item * + Fixed ISE that occurred when certain bad date formats were used for Cover Dates on stories. [Michael Peters] Modified: trunk/krang/lib/Krang/CGI/Category.pm =================================================================== --- trunk/krang/lib/Krang/CGI/Category.pm 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/lib/Krang/CGI/Category.pm 2010-03-08 00:06:20 UTC (rev 6346) @@ -223,39 +223,43 @@ # try saving eval { $category->save(); }; - # is there an existing category or story with our URL? - if ($@ and ref($@) and $@->isa('Krang::Category::DuplicateURL')) { + # it was an exception + if ($@ and ref($@)) { + # is there an existing category or story with our URL? + if ($@->isa('Krang::Category::DuplicateURL')) { + if ($@->category_id) { + # there's an existing category... + add_alert( + 'duplicate_url', + url => $@->url, + category_id => $@->category_id + ); + return $self->new_category(bad => ['parent_id', 'dir']); + } elsif ($@->story_id) { + # there's an existing story; turn it into a category cover + # (after making sure we can edit it!) + my ($story) = pkg('Story')->find(story_id => $@->story_id); - if ($@->category_id) { - - # there's an existing category... - add_alert( - 'duplicate_url', - url => $@->url, - category_id => $@->category_id - ); + unless ($story->turn_into_category_index(category => $category, steal => 1)) { + add_alert('uneditable_story_has_url', id => $story->story_id); + return $self->new_category(bad => ['parent_id', 'dir']); + } + add_message( + 'story_had_category_url', + id => $story->story_id, + cat_id => $category->{category_id} + ); + } else { + croak("DuplicateURL didn't include category_id OR story_id!"); + } + } elsif ($@->isa('Krang::Category::ReservedURL')) { + add_alert('reserved_url', reserved => $@->reserved); return $self->new_category(bad => ['parent_id', 'dir']); - - } elsif ($@->story_id) { - - # there's an existing story; turn it into a category cover - # (after making sure we can edit it!) - my ($story) = pkg('Story')->find(story_id => $@->story_id); - - unless ($story->turn_into_category_index(category => $category, steal => 1)) { - add_alert('uneditable_story_has_url', id => $story->story_id); - return $self->new_category(bad => ['parent_id', 'dir']); - } - add_message( - 'story_had_category_url', - id => $story->story_id, - cat_id => $category->{category_id} - ); } else { - croak("DuplicateURL didn't include category_id OR story_id!"); + # rethrow + die($@); } } elsif ($@) { - # rethrow die($@); } Modified: trunk/krang/lib/Krang/CGI/Story.pm =================================================================== --- trunk/krang/lib/Krang/CGI/Story.pm 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/lib/Krang/CGI/Story.pm 2010-03-08 00:06:20 UTC (rev 6346) @@ -318,17 +318,22 @@ ); }; - # is it a dup? - if ($@ and ref($@) and $@->isa('Krang::Story::DuplicateURL')) { - - my $class = pkg('ElementLibrary')->top_level(name => $type); - $self->alert_duplicate_url(error => $@, class => $class); - return $self->new_story(bad => ['category_id', $class->url_attributes]); - + # it's an exception + if ($@ and ref($@)) { + if ($@->isa('Krang::Story::DuplicateURL')) { + # it's a dup + my $class = pkg('ElementLibrary')->top_level(name => $type); + $self->alert_duplicate_url(error => $@, class => $class); + return $self->new_story(bad => ['category_id', $class->url_attributes]); + } elsif ($@->isa('Krang::Story::ReservedURL')) { + # it's a reserved url + add_alert('reserved_url', reserved => $@->reserved); + return $self->new_story(bad => ['slug']); + } else { + die($@); # rethrow + } } elsif ($@) { - - # rethrow - die($@); + die($@); # rethrow } # save it @@ -2301,14 +2306,18 @@ # handle edit save errors (used by db_save, db_save_and_stay, and revert) sub add_save_alert { my ($self, $story, $error) = @_; - if (ref($error) and $error->isa('Krang::Story::DuplicateURL')) { - $self->alert_duplicate_url(error => $error, class => $story->class); + if (ref $error) { + if ($error->isa('Krang::Story::DuplicateURL')) { + $self->alert_duplicate_url(error => $error, class => $story->class); + } elsif ($error->isa('Krang::Story::ReservedURL')) { + add_alert('reserved_url', reserved => $error->reserved); + } else { + die($error); # rethrow + } } elsif (ref($error) and $error->isa('Krang::Story::MissingCategory')) { add_alert('missing_category_on_save'); } else { - - # rethrow - die($error); + die($error); # rethrow } } @@ -2487,7 +2496,6 @@ } sub update_categories { - my ($self, %args) = @_; my $query = $args{query}; my $story = $args{story}; @@ -2521,33 +2529,34 @@ # slug is safe on current cats, so now let's try the new cats eval { $story->categories(@new_cats) }; if (!$@) { - - # success! - return 1; - + return 1; # success! } else { + if (ref $@) { + if ($@->isa('Krang::Story::DuplicateURL')) { + $self->alert_duplicate_url( + error => $@, + class => $story->class, + added_cats => \@added_cats + ); + eval { $story->categories(@old_cats) }; - # failure... - if (ref($@) and $@->isa('Krang::Story::DuplicateURL')) { + # if slug has changed, even the old categories may fail... + if ($@ && ($new_slug ne $old_slug)) { + $story->slug($old_slug); # revert slug just long + $story->categories(@old_cats); # enough to load old URLs + $story->slug($new_slug); # and return user to Edit + } - $self->alert_duplicate_url( - error => $@, - class => $story->class, - added_cats => \@added_cats - ); - eval { $story->categories(@old_cats) }; - - # if slug has changed, even the old categories may fail... - if (@$ && ($new_slug ne $old_slug)) { - $story->slug($old_slug); # revert slug just long - $story->categories(@old_cats); # enough to load old URLs - $story->slug($new_slug); # and return user to Edit + # in either case - return failure + return 0; + } elsif ($@->isa('Krang::Story::ReservedURL')) { + add_alert('reserved_url', reserved => $@->reserved); + return 0; + } else { + die $@; } - - # in either case - return failure - return 0; } else { - die($@); + die $@; } } } @@ -2595,17 +2604,14 @@ } sub process_slug_input { - my ($self, %args) = @_; - - my $slug = $args{slug}; - my $story = $args{story}; - my $type = $args{type} || ($story && $story->class->name); - my $cat_idx = $args{cat_idx}; - my @categories = $args{categories} && @{$args{categories}}; - + my $slug = $args{slug}; + my $story = $args{story}; + my $type = $args{type} || ($story && $story->class->name); + my $cat_idx = $args{cat_idx}; + my @categories = $args{categories} && @{$args{categories}}; my $slug_entry_for_type = pkg('ElementLibrary')->top_level(name => $type)->slug_use(); - my $slug_required = ($slug_entry_for_type eq 'require'); + my $slug_required = ($slug_entry_for_type eq 'require'); my $slug_optional = (($slug_entry_for_type eq 'encourage') || ($slug_entry_for_type eq 'discourage')); @@ -2619,27 +2625,31 @@ add_alert('no_slug_no_cat_idx'); return 0; } elsif ($story && ($story->slug ne $slug)) { - # and if we've been given categories to check against new slug... if (@categories) { - # store old slug/categories in case we need to revert my $old_slug = $story->slug; my @old_cats = $story->categories; - # try out new slug on category list to see if it causes any dupes + # try out new slug on category list to see if it causes any dupes or is reserved $story->slug($slug); eval { $story->categories(@categories) }; - if ($@ and ref($@) and $@->isa('Krang::Story::DuplicateURL')) { - $self->alert_duplicate_url(error => $@, class => $story->class); - $story->slug($old_slug); - $story->categories(@old_cats); - return 0; + if ($@ && ref $@) { + if($@->isa('Krang::Story::DuplicateURL')) { + $self->alert_duplicate_url(error => $@, class => $story->class); + $story->slug($old_slug); + $story->categories(@old_cats); + return 0; + } elsif( $@->isa('Krang::Story::ReservedURL')) { + add_alert('reserved_url', reserved => $@->reserved); + return 0; + } else { + die $@; + } } elsif ($@) { - die($@); + die $@; } } else { - # even if we're not checking categories, update slug $story->slug($slug); } @@ -2650,9 +2660,7 @@ } sub alert_duplicate_url { - my ($self, %args) = @_; - my $class = $args{class}; my $error = $args{error}; my $added_cats = $args{added_cats}; @@ -2817,9 +2825,8 @@ =cut sub unretire { - my $self = shift; - my $q = $self->query; - + my $self = shift; + my $q = $self->query; my $story_id = $q->param('story_id'); croak("No story_id found in CGI params when trying to unretire story.") @@ -2831,12 +2838,11 @@ croak("Unable to load story '" . $story_id . "'.") unless $story; - eval { $story->unretire() }; # may through a Krang::Story::DuplicateURL exception - my @conflict_cats = (); my @conflict_stories = (); + eval { $story->unretire() }; # may through an exception - if ($@ and ref($@) and $@->isa('Krang::Story::DuplicateURL')) { + if ($@ && ref($@) && $@->isa('Krang::Story::DuplicateURL')) { if ($@->categories) { @conflict_cats = @{$@->categories}; } elsif ($@->stories) { @@ -2910,6 +2916,11 @@ $session{story} = $story; return $self->edit; + } elsif ($@ && ref($@) && $@->isa('Krang::Story::ReservedURL')) { + add_alert('reserved_url', reserved => $@->reserved); + return $self->edit; + } elsif($@) { + die $@; } add_message('story_unretired', id => $story_id, url => $story->url); Modified: trunk/krang/lib/Krang/Category.pm =================================================================== --- trunk/krang/lib/Krang/Category.pm 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/lib/Krang/Category.pm 2010-03-08 00:06:20 UTC (rev 6346) @@ -98,6 +98,7 @@ use Exception::Class ( 'Krang::Category::Dependent' => {fields => 'dependents'}, 'Krang::Category::DuplicateURL' => {fields => [qw(category_id story_id url)]}, + 'Krang::Category::ReservedURL' => {fields => [qw(reserved)]}, 'Krang::Category::NoEditAccess' => {fields => [qw(category_id category_url)]}, 'Krang::Category::RootDeletion', 'Krang::Category::CopyAssetConflict', @@ -111,13 +112,13 @@ ################### use Krang::ClassLoader DB => qw(dbh); use Krang::ClassLoader Element => qw(foreach_element); - use Krang::ClassLoader 'Media'; use Krang::ClassLoader 'Story'; use Krang::ClassLoader 'Template'; use Krang::ClassLoader 'Group'; use Krang::ClassLoader 'UUID'; use Krang::ClassLoader Log => qw(debug assert ASSERT); +use Krang::ClassLoader Conf => qw(ReservedURLs); # # Package Variables @@ -630,6 +631,45 @@ return 0; } +=item * $category->reserved_check() + +This method checks to see if URL of this category clashes with a reserved +URL as specified by the C<ReservedURLs> configuration directive. If it +conflicts, then a C<Krang::Category::ReservedURL> exception will be thrown. + + eval { $self->reserved_check() }; + if ($@ and $@->isa('Krang::Category::ReservedURL')) { + croak("The 'url' of this category is reserved"); + } + +=cut + +sub reserved_check { + my $self = shift; + my $url = $self->{url}; + + # make sure they end with a slash + $url = "$url/" unless $url =~ /\/$/; + $url = "$url/" unless $url =~ /\/$/; + + # create a relative version of this url + my $relative_url = $url; + $relative_url =~ s/^[^\/]+\//\//; + + # now compare them to the configured ReservedURLs + foreach my $reserved (split(/\s+/, ReservedURLs)) { + $reserved = "$reserved/" unless $reserved =~ /\/$/; + my $compare = $reserved =~ /^\// ? $relative_url : $url; + # throw exception + Krang::Category::ReservedURL->throw( + message => "Reserved URL ($reserved)", + reserved => $reserved, + ) if $compare eq $reserved; + } + + return 0; +} + =item * @categories = $category->ancestors() =item * @category_ids = $category->ancestors( ids_only => 1 ) @@ -1057,6 +1097,9 @@ # duplicate is found $self->duplicate_check(); + # check for reserved urls + $self->reserved_check(); + # save element, get id back my $element = $self->element; $element->save(); Modified: trunk/krang/lib/Krang/Conf.pm =================================================================== --- trunk/krang/lib/Krang/Conf.pm 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/lib/Krang/Conf.pm 2010-03-08 00:06:20 UTC (rev 6346) @@ -63,6 +63,7 @@ PasswordChangeTime PasswordChangeCount PreviewSSL + ReservedURLs RewriteLogLevel SavedVersionsPerMedia SavedVersionsPerStory Modified: trunk/krang/lib/Krang/Story.pm =================================================================== --- trunk/krang/lib/Krang/Story.pm 2010-03-07 00:11:58 UTC (rev 6345) +++ trunk/krang/lib/Krang/Story.pm 2010-03-08 00:06:20 UTC (rev 6346) @@ -1,18 +1,16 @@ package Krang::Story; -use Krang::ClassFactory qw(pkg); use strict; use warnings; - +use Krang::ClassFactory qw(pkg); use Krang::ClassLoader Element => qw(foreach_element); use Krang::ClassLoader 'Category'; -use Krang::ClassLoader History => qw( add_history ); +use Krang::ClassLoader History => qw(add_history); use Krang::ClassLoader Log => qw(assert ASSERT affirm debug info critical); -use Krang::ClassLoader Conf => qw(SavedVersionsPerStory); +use Krang::ClassLoader Conf => qw(SavedVersionsPerStory ReservedURLs); use Krang::ClassLoader DB => qw(dbh); use Krang::ClassLoader Session => qw(%session); use Krang::ClassLoader 'Pref'; use Krang::ClassLoader 'UUID'; - use Krang::ClassLoader Localization => qw(localize); use Krang::ClassLoader 'Trash'; use Carp qw(croak); @@ -23,6 +21,7 @@ # setup exceptions use Exception::Class 'Krang::Story::DuplicateURL' => {fields => ['stories', 'categories']}, + 'Krang::Story::ReservedURL' => {fields => ['reserved']}, 'Krang::Story::MissingCategory' => {fields => []}, 'Krang::Story::NoCategoryEditAccess' => {fields => ['category_id']}, 'Krang::Story::NoEditAccess' => {fields => ['story_id']}, @@ -390,10 +389,10 @@ @categories = $story->categories; -This method may throw a Krang::Story::DuplicateURL exception if you +This method may throw a C<Krang::Story::DuplicateURL> exception if you add a new category and it generates a duplicate URL. When this exception is thrown the category list is still changed and you may -continue to operate on the story. However, if you try to call save() +continue to operate on the story. However, if you try to call C<save()> you will receive the same exception. =cut @@ -403,7 +402,6 @@ # get unless (@_) { - # load the cache as necessary for (0 .. $#{$self->{category_ids}}) { next if $self->{category_cache}[$_]; @@ -430,9 +428,9 @@ $self->{url_cache} = []; unless ($self->{slug} eq '_TEMP_SLUG_FOR_CONVERSION_') { - # make sure this change didn't cause a conflict $self->_verify_unique(); + $self->_verify_reserved(); } } @@ -549,8 +547,8 @@ required. After this call the object is guaranteed to be in a valid state and may be saved immediately with C<save()>. -Will throw a Krang::Story::DuplicateURL exception with a story_id -or category_id field if saving this story would conflict with an +Will throw a C<Krang::Story::DuplicateURL> exception with a C<story_id> +or C<category_id> field if saving this story would conflict with an existing story or category. @@ -746,11 +744,11 @@ If the story is not checked out by the user attempting the save, then an error will be thrown unless C<no_verify_checkout> is true. -Will throw a C<Krang::Story::DuplicateURL> exception with a story_id field +Will throw a C<Krang::Story::DuplicateURL> exception with a C<story_id> field if saving this story would conflict with an existing story or category. Will throw a C<Krang::Story::MissingCategory> exception if this story -doesn't have at least one category. This can happen when a clone() +doesn't have at least one category. This can happen when a C<clone()> results in a story with no categories. Will throw a C<Krang::Story::NoCategoryEditAccess> exception if the @@ -791,6 +789,9 @@ # make sure it's got a unique URL $self->_verify_unique(); + # make sure it's not a reserved URL + $self->_verify_reserved(); + # update the version number $self->{version}++ unless $args{keep_version}; } @@ -983,6 +984,31 @@ } } +# makes sure this story doesn't have a reserved URL +sub _verify_reserved { + my $self = shift; + foreach my $url ($self->urls) { + # make sure they end with a slash + $url = "$url/" unless $url =~ /\/$/; + $url = "$url/" unless $url =~ /\/$/; + + # create a relative version of this url + my $relative_url = $url; + $relative_url =~ s/^[^\/]+\//\//; + + # now compare them to the configured ReservedURLs + foreach my $reserved (split(/\s+/, ReservedURLs)) { + $reserved = "$reserved/" unless $reserved =~ /\/$/; + my $compare = $reserved =~ /^\// ? $relative_url : $url; + # throw exception + Krang::Story::ReservedURL->throw( + message => "Reserved URL ($reserved)", + reserved => $reserved, + ) if $compare eq $reserved; + } + } +} + =item C<< @stories = Krang::Story->find(title => "Turtle Soup") >> =item C<< @story_ids = Krang::Story->find(title => "Turtle Soup", ids_only => 1) >> @@ -2937,6 +2963,9 @@ # make sure no other story occupies our initial place (URL) $self->_verify_unique; + # make sure it's not now a reserved URL + $self->_verify_reserved(); + # unretire the story my $dbh = dbh(); $dbh->do('UPDATE story SET retired = 0 WHERE story_id = ?', undef, $self->{story_id}); @@ -3033,8 +3062,12 @@ story_id => $self->story_id ) unless ($self->may_edit); - # make sure no other story occupies our initial place (URL) - $self->_verify_unique unless $self->retired; + unless($self->retired) { + # make sure no other story occupies our initial place (URL) + $self->_verify_unique; + # make sure this isn't now a reserved URL + $self->_verify_reserved(); + } # make sure we are the one $self->checkout; |