123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 |
- # Copyright (C) 2004, 2005, 2007 Alex Schroeder <alex@emacswiki.org>
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- use strict;
- use v5.10;
- AddModuleDescription('localnames.pl', 'Local Names Extension');
- our ($q, $Now, %Page, %Action, $OpenPageName, $ScriptName, $DataDir, $RssDir, @MyRules, @MyMaintenance, @MyInitVariables, $FullUrlPattern, $FreeLinkPattern, $CommentsPrefix, $UseCache, @UserGotoBarPages, %AdminPages, @MyAdminCode, @MyFooters, $UsePathInfo);
- =encoding utf8
- =head1 Local Names
- This module allows you to centrally define redirections. Thus you can
- define that whenever somebody links to the page Foo the link will
- point to http://example.com/. These redirects are defined on special
- page called LocalNames. You can change the name of that page by
- setting C<$LocalNamesPage>.
- You can also link to external lists of such redirections, as long as
- they use the namespace description format developed by Lion Kimbro.
- Basically you can "import" redirections. These external lists are
- cached in a directory called C<ln> inside the data directory. You can
- change the directory by setting C<$LnDir>.
- =cut
- our ($LocalNamesPage, %LocalNames, $LocalNamesCollect,
- $LocalNamesCollectMaxWords, $LnDir, $LnCacheHours,
- %WantedPages);
- $LocalNamesPage = 'LocalNames';
- $LocalNamesCollect = 0;
- $LocalNamesCollectMaxWords = 2;
- # LN caching is written very similar to the RSS file caching
- $LnDir = "$DataDir/ln";
- $LnCacheHours = 12;
- sub GetLnFile {
- return $LnDir . '/' . UrlEncode(shift);
- }
- =head2 Maintenance
- Whenever maintenance runs, all the cached external lists of
- redirections are deleted whenever they are older than twelve hours.
- You can change this expiry time by setting C<$LnCacheHours>.
- =cut
- push (@MyMaintenance, \&LnMaintenance);
- sub LnMaintenance {
- if (opendir(DIR, encode_utf8($RssDir))) { # cleanup if they should expire anyway
- foreach my $file (readdir(DIR)) {
- unlink("$RssDir/$file") if -M $file > $LnCacheHours * 3600;
- }
- closedir DIR;
- }
- }
- =head2 Defining Local Names
- Local Names are defined on the LocalNames page.
- If you create ordinary named external links such as
- C<[http://ln.taoriver.net/ Local Names Website]> on the LocalNames
- page, you will have defined a new Local Name. If you write C<[[Local
- Names Website]]> elsewhere on the site (and the page does not exist),
- that link will point to the website you specified.
- You can link from the LocalNames page to existing namespace
- descriptions. These other namespace descriptions must use the
- namespace description format developed by Lion Kimbro. If you write
- C<[[ln:URL]]> or C<[[ln:URL text]]>, this will import all the Local
- Names defined there into your wiki.
- Example: C<[[ln:http://ln.taoriver.net/localnames.txt Lion's Example
- Localnames List]]>.
- Currently only LN records with absolute URLs are parsed correctly. All
- other record types are ignored.
- If you want to learn more about local names, see
- L<http://ln.taoriver.net/>.
- =cut
- # render [[ln:url]] as something clickable
- push(@MyRules, \&LocalNamesRule);
- sub LocalNamesRule {
- if (m/\G\[\[ln:$FullUrlPattern\s*([^\]]*)\]\]/cg) {
- # [[ln:url text]], [[ln:url]]
- return $q->a({-class=>'url outside ln', -href=>$1}, $2||$1);
- }
- return;
- }
- =head2 Initialization
- The LocalNames page is added to C<%AdminPages> so that the
- Administration page will list a link to it. The LocalNames page will
- be read and parsed for every request. The result is that the
- C<%LocalNames> hash has pagenames as keys and URLs to redirect to as
- values.
- If the LocalNames page refers to external lists of redirections, these
- will be read from the cache or fetched anew if older than twelve
- hours. If you use the cache=0 parameter in an URL or set C<$UseCache>
- to zero or less, Oddmuse will B<fetch the lists of redirections every
- single time>. Using the cache=0 parameter is a way to force Oddmuse to
- expire the cache. Setting C<$UseCache> to 0 should not be used on a
- live site.
- Definitions of redirections on the LocalNames take precedence over
- redirections defined on remote sites. Earlier lists of redirections
- take precedence over later lists.
- We ignore the spec at L<http://ln.taoriver.net/spec-1.2.html#Syntax>
- when considering what names we allow, since Oddmuse will parse them as
- regular links anyway.
- =cut
- push(@MyInitVariables, \&LocalNamesInit);
- sub LocalNamesInit {
- %WantedPages = (); # list of missing pages used during this request
- %LocalNames = ();
- $LocalNamesPage = FreeToNormal($LocalNamesPage); # spaces to underscores
- $AdminPages{$LocalNamesPage} = 1;
- my $data = GetPageContent($LocalNamesPage);
- while ($data =~ m/\[$FullUrlPattern\s+([^\]]+?)\]/g) {
- my ($page, $url) = ($2, $1);
- my $id = FreeToNormal($page);
- $LocalNames{$id} = $url;
- }
- # Now read data from ln links, checking cache if possible. For all
- # URLs not in the cache or with invalid cache, fetch the file again,
- # and save it in the cache.
- my @ln = $data =~ m/\[\[ln:$FullUrlPattern[^\]]*?\]\]/g;
- my %todo = map {$_, GetLnFile($_)} @ln;
- my %data = ();
- if (GetParam('cache', $UseCache) > 0) {
- foreach my $uri (keys %todo) { # read cached rss files if possible
- if ($Now - Modified($todo{$uri}) < $LnCacheHours * 3600) {
- $data{$uri} = ReadFile($todo{$uri});
- delete($todo{$uri}); # no need to fetch them below
- }
- }
- }
- my @need_cache = keys %todo;
- if (keys %todo > 1) { # try parallel access if available
- eval { # see code example in LWP::Parallel, not LWP::Parllel::UserAgent (no callbacks here)
- require LWP::Parallel::UserAgent;
- my $pua = LWP::Parallel::UserAgent->new();
- foreach my $uri (keys %todo) {
- if (my $res = $pua->register(HTTP::Request->new('GET', $uri))) {
- warn $res->error_as_HTML;
- }
- }
- %todo = (); # because the uris in the response may have changed due to redirects
- my $entries = $pua->wait();
- foreach (keys %$entries) {
- my $uri = $entries->{$_}->request->uri;
- $data{$uri} = $entries->{$_}->response->content;
- }
- }
- }
- foreach my $uri (keys %todo) { # default operation: synchronous fetching
- $data{$uri} = GetRaw($uri);
- }
- if (GetParam('cache', $UseCache) > 0) {
- CreateDir($LnDir);
- foreach my $uri (@need_cache) {
- WriteStringToFile(GetLnFile($uri), $data{$uri});
- }
- }
- # go through the urls in the right order, this time
- foreach my $ln (@ln) {
- my ($previous_type, $previous_url);
- foreach my $line (split(/[\r\n]+/, $data{$ln})) {
- if ($line =~ /^LN\s+"$FreeLinkPattern"\s+(?:"$FullUrlPattern"|\.)$/
- or $previous_type eq 'LN'
- and $line =~ /^\.\s+"$FreeLinkPattern"\s+(?:"$FullUrlPattern"|\.)$/) {
- my ($name, $url) = ($1, $2);
- $url = $previous_url if not $url and $previous_url;
- $previous_url = $url;
- $previous_type = 'LN';
- my $id = FreeToNormal($name);
- # Only store this, if not already stored!
- if (not $LocalNames{$id}) {
- $LocalNames{$id} = $url;
- }
- } else {
- $previous_type = undef;
- }
- # elsif ($line =~ /^NS "(.*)" "$FullUrlPattern"$/g) {
- # }
- }
- }
- }
- =head2 Name Resolution
- We want Near Links only to have an effect for pages that do not exist
- locally. It should not take precedence! Thus, we hook into
- C<ResolveId>; this function returns a list of four elements: CSS
- class, resolved id, title (eg. for popups), and a boolean saying
- whether the page exists or not. If the second element is empty, then
- no page exists and we check C<%LocalNames> for a match. If there is a
- match, we return the URL using the CSS class "near" and the title
- "LocalNames". The CSS class is the same that is used for Near Links
- because the effect is so similar.
- Note: Existing local pages take precedence over local names, but local
- names take precedence over Near Links.
- We also keep track of wanted pages (links to missing pages) so that we
- can printe a list of definition links at the bottom using the Define
- Action (see below).
- =cut
- *OldLocalNamesResolveId = \&ResolveId;
- *ResolveId = \&NewLocalNamesResolveId;
- sub NewLocalNamesResolveId {
- my $id = shift;
- my ($class, $resolved, @rest) = OldLocalNamesResolveId($id, @_);
- if ((not $resolved or $class eq 'near') and $LocalNames{$id}) {
- return ('near', $LocalNames{$id}, $LocalNamesPage);
- } else {
- $WantedPages{$id} = 1 if not $resolved; # this is provisional!
- return ($class, $resolved, @rest);
- }
- }
- =head2 Automatically Defining Local Names
- It is possible to have Oddmuse automatically define local names as you
- edit pages. In order to enable this, set C<$LocalNamesCollect> to 1.
- Once you this, every time you save a page with a named external link
- such as C<[http://www.emacswiki.org/alex/ Alex]>, this will add or
- update the corresponding entry on the LocalNames page.
- In order to reduce the number of entries thus collected, only external
- links with a name consisting of one or two words are used. You can
- change this word limit by setting C<$LocalNamesCollectMaxWords>.
- The default limit of two words assumes that you might want to make
- C<Alex> a link, or C<Alex Schroeder>, but not C<the example on Alex’s
- blog> (five “words”, since the code looks at whitespace only).
- =cut
- *LocalNamesOldSave = \&Save;
- *Save = \&LocalNamesNewSave;
- sub LocalNamesNewSave {
- LocalNamesOldSave(@_);
- my ($currentid, $text) = @_;
- # avoid recursion
- return if $currentid eq $LocalNamesPage or not $LocalNamesCollect;
- my $currentname = $currentid;
- $currentname =~ s/_/ /g;
- local ($OpenPageName, %Page);
- OpenPage($LocalNamesPage);
- my $localnames = $Page{text};
- my %map = ();
- while ($text =~ /\[$FullUrlPattern\s+(([^ \]]+?\s*){1,$LocalNamesCollectMaxWords})\]/g) {
- my ($page, $url) = ($2, $1);
- my $id = FreeToNormal($page);
- $map{$id} = () unless defined $map{$id};
- $map{$id}{$url} = 1;
- }
- my %collection = ();
- foreach my $id (keys %map) {
- # canonical form with trimmed spaces and no underlines
- my $page = $id;
- $page =~ s/_/ /g;
- # skip if the mapping from id to url already defined matches at
- # least one of the definitions on the current page.
- next if $map{$id}{$LocalNames{$id}};
- $collection{$page} = 1;
- # pick a random url from the list
- my @urls = keys %{$map{$id}};
- my $url = $urls[0];
- # if a different mapping exists already; change the old mapping to the new one
- # if the change fails (eg. the page name is not in canonical form), don't skip!
- next if $LocalNames{$id}
- and $localnames =~ s/\[$LocalNames{$id}\s+$page\]/[$url $page]/g;
- # add a new entry at the end
- $localnames .= "\n\n* [$url $page]"
- . Ts(" -- defined on %s", "[[$currentname]]");
- $LocalNames{$id} = $url; # prevent multiple additions
- }
- # minor change
- my @collection = sort keys %collection;
- Save($LocalNamesPage, $localnames,
- Tss("Local names defined on %1: %2", $currentname,
- length(@collection > 1)
- ? join(', and ',
- join(', ', @collection[0 .. $#collection-1]),
- $collection[-1])
- : @collection), 1)
- unless $localnames eq $Page{text};
- }
- =head2 Local Names Format
- The Ln Action lists all the local pages in the local names format
- defined in the specification. Example URL:
- C<http://localhost/cgi-bin/wiki?action=ln>.
- If you want to learn more about local names and the format used, see
- L<http://ln.taoriver.net/>.
- =cut
- $Action{ln} = \&DoLocalNames;
- sub DoLocalNames {
- print GetHttpHeader('text/plain');
- print "X VERSION 1.2\n";
- print "# Local Pages\n";
- foreach my $id (AllPagesList()) {
- my $title = $id;
- $title =~ s/_/ /g;
- my $url = $ScriptName . ($UsePathInfo ? '/' : '?') . $id;
- print qq{LN "$title" "$url"\n};
- }
- if (GetParam('expand', 0)) {
- print "# Local names defined on $LocalNamesPage:\n";
- my $data = GetPageContent($LocalNamesPage);
- while ($data =~ m/\[$FullUrlPattern\s+([^\]]+?)\]/g) {
- my ($title, $url) = ($2, $1);
- my $id = FreeToNormal($title);
- print qq{LN "$title" "$url"\n};
- }
- print "# Namespace delegations defined on $LocalNamesPage:\n";
- while ($data =~ m/\[\[ln:$FullUrlPattern([^\]]*)?\]\]/g) {
- my ($title, $url) = ($2, $1);
- my $id = FreeToNormal($title);
- print qq{NS "$title" "$url"\n};
- }
- } else {
- print "# Local names defined on $LocalNamesPage:\n";
- foreach my $id (keys %LocalNames) {
- my $title = $id;
- $title =~ s/_/ /g;
- print qq{LN "$title" "$LocalNames{$id}"\n};
- }
- }
- }
- =head2 Define Action
- The Define Action allows you to interactively add local names using a
- form. Example URL: C<http://localhost/cgi-bin/wiki?action=define>.
- You can also provide the C<name> and C<link> parameters yourself if
- you want to use this action from a script.
- As wanted pages (links to missing pages) come up, you will get links
- to appropriate define actions in your footer.
- =cut
- $Action{define} = \&DoDefine;
- sub DoDefine {
- if (GetParam('link', '') and GetParam('name', '')) {
- SetParam('title', $LocalNamesPage);
- SetParam('text', GetPageContent($LocalNamesPage) . "\n* ["
- . GetParam('link', '') . ' ' . GetParam('name', '')
- . "]\n");
- SetParam('summary', 'Defined ' . GetParam('name'));
- return DoPost($LocalNamesPage);
- } else {
- print GetHeader('', T('Define')),
- $q->start_div({-class=>'content define'}),
- GetFormStart(undef, 'get', 'def');
- my $go = T('Go!');
- print $q->p($q->label({-for=>"defined"}, T('Name:') . ' '),
- $q->textfield(-name=>"name", -id=>"defined",
- -tabindex=>"1", -size=>20));
- print $q->p($q->label({-for=>"definition"}, T('URL:') . ' '),
- $q->textfield(-name=>"link", -id=>"definition",
- -tabindex=>"2", -size=>20));
- print $q->p($q->submit(-label=>$go, -tabindex=>"3"),
- GetHiddenValue('action', 'define'),
- GetHiddenValue('recent_edit', 'on'));
- print $q->end_form, $q->end_div();
- PrintFooter();
- }
- }
- push(@MyAdminCode, sub {
- my ($id, $menuref, $restref) = @_;
- push(@$menuref, ScriptLink('action=define', T('Define Local Names'),
- 'define'));
- });
- # link to define action for non-existing pages
- push(@MyFooters, \&GetWantedPages);
- sub GetWantedPages {
- # skip admin pages
- foreach my $id (@UserGotoBarPages, keys %AdminPages) {
- delete $WantedPages{$id};
- }
- # skip comment pages
- if ($CommentsPrefix) {
- foreach my $id (keys %WantedPages) {
- delete $WantedPages{$id} if $id =~ /^$CommentsPrefix/; # TODO use $CommentsPattern ?
- }
- }
- # now something more complicated: if near-links.pl was loaded, then
- # %WantedPages may contain pages that will in fact resolve. That's
- # why we try to resolve all the wanted ids again. And since
- # resolving ids will do stuff to %WantedPages, we need to make a
- # copy of the ids we're looking at.
- my @wanted;
- foreach my $id (keys %WantedPages) {
- my ($class, $resolved) = ResolveId($id);
- push(@wanted, $id) unless $resolved;
- }
- # if any wanted pages remain, print them
- if (@wanted) {
- return $q->div({-class=>'definition'},
- $q->p(T('Define external redirect:'), ' ',
- map { my $page = NormalToFree($_);
- ScriptLink('action=define;name='
- . UrlEncode($page),
- $page,
- 'define');
- } @wanted));
- }
- return '';
- }
|