diff options
Diffstat (limited to 'mod')
-rw-r--r-- | mod/.gitignore | 2 | ||||
-rw-r--r-- | mod/buildfile | 55 | ||||
-rw-r--r-- | mod/database | 26 | ||||
-rw-r--r-- | mod/database-module | 55 | ||||
-rw-r--r-- | mod/database-module.cxx | 50 | ||||
-rw-r--r-- | mod/database.cxx | 60 | ||||
-rw-r--r-- | mod/diagnostics | 306 | ||||
-rw-r--r-- | mod/diagnostics.cxx | 30 | ||||
-rw-r--r-- | mod/mod-package-details | 42 | ||||
-rw-r--r-- | mod/mod-package-details.cxx | 258 | ||||
-rw-r--r-- | mod/mod-package-search | 42 | ||||
-rw-r--r-- | mod/mod-package-search.cxx | 181 | ||||
-rw-r--r-- | mod/mod-package-version-details | 45 | ||||
-rw-r--r-- | mod/mod-package-version-details.cxx | 320 | ||||
-rw-r--r-- | mod/mod-repository-details | 42 | ||||
-rw-r--r-- | mod/mod-repository-details.cxx | 147 | ||||
-rw-r--r-- | mod/mod-repository-root | 60 | ||||
-rw-r--r-- | mod/mod-repository-root.cxx | 263 | ||||
-rw-r--r-- | mod/module | 201 | ||||
-rw-r--r-- | mod/module.cxx | 410 | ||||
-rw-r--r-- | mod/options-types | 32 | ||||
-rw-r--r-- | mod/options.cli | 211 | ||||
-rw-r--r-- | mod/page | 403 | ||||
-rw-r--r-- | mod/page.cxx | 693 | ||||
-rw-r--r-- | mod/services.cxx | 15 | ||||
-rw-r--r-- | mod/types-parsers | 57 | ||||
-rw-r--r-- | mod/types-parsers.cxx | 114 |
27 files changed, 4120 insertions, 0 deletions
diff --git a/mod/.gitignore b/mod/.gitignore new file mode 100644 index 0000000..ddd62b8 --- /dev/null +++ b/mod/.gitignore @@ -0,0 +1,2 @@ +options +options.?xx diff --git a/mod/buildfile b/mod/buildfile new file mode 100644 index 0000000..574b34a --- /dev/null +++ b/mod/buildfile @@ -0,0 +1,55 @@ +# file : mod/buildfile +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +define mod: libso +mod{*}: bin.libprefix = mod_ +mod{*}: install = libexec + +import libs += libodb%lib{odb} +import libs += libodb-pgsql%lib{odb-pgsql} +import libs += libbpkg%lib{bpkg} +import libs += libstudxml%lib{studxml} + +include ../brep/ + +mod{brep}: \ + {hxx cxx}{ database } \ + {hxx cxx}{ database-module } \ + {hxx cxx}{ diagnostics } \ + {hxx cxx}{ mod-package-details } \ + {hxx cxx}{ mod-package-search } \ + {hxx cxx}{ mod-package-version-details } \ + {hxx cxx}{ mod-repository-details } \ + {hxx cxx}{ mod-repository-root } \ + {hxx cxx}{ module } \ + {hxx ixx cxx}{ options } \ + {hxx }{ options-types } \ + {hxx cxx}{ page } \ + { cxx}{ services } \ + {hxx cxx}{ types-parsers } \ + ../web/{hxx cxx}{ mime-url-encoding } \ + ../web/{hxx }{ module } \ + ../web/{hxx }{ xhtml } \ + ../web/{hxx cxx}{ xhtml-fragment } \ +../web/apache/{hxx }{ log } \ +../web/apache/{hxx ixx cxx}{ request } \ +../web/apache/{hxx txx cxx}{ service } \ +../web/apache/{hxx }{ stream } \ +../brep/lib{brep} $libs + +# Don't install any of the module's headers. +# +{hxx ixx txx}{*}: install = false +../web/{hxx ixx txx}{*}: install = false + +# Set option prefix to the empty value to handle all unknown request parameters +# uniformly with a single catch block. +# +cli.options += --std c++11 -I $src_root --include-with-brackets \ +--include-prefix mod --guard-prefix MOD \ +--cxx-prologue "#include <mod/types-parsers>" \ +--cli-namespace brep::cli --generate-file-scanner --suppress-usage \ +--generate-modifier --generate-description --option-prefix "" + +{hxx ixx cxx}{options}: cli{options} diff --git a/mod/database b/mod/database new file mode 100644 index 0000000..2730449 --- /dev/null +++ b/mod/database @@ -0,0 +1,26 @@ +// file : mod/database -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_DATABASE +#define MOD_DATABASE + +#include <odb/forward.hxx> // database + +#include <brep/types> +#include <brep/utility> + +#include <mod/options> + +namespace brep +{ + // Returns pointer to the shared database instance, creating one on the + // first call. On subsequent calls ensures passed host and port equals + // to ones of the existing database instance throwing runtime_error + // otherwise. Is not thread-safe. + // + shared_ptr<odb::core::database> + shared_database (const options::db&); +} + +#endif // MOD_DATABASE diff --git a/mod/database-module b/mod/database-module new file mode 100644 index 0000000..0933794 --- /dev/null +++ b/mod/database-module @@ -0,0 +1,55 @@ +// file : mod/database-module -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_DATABASE_MODULE +#define MOD_DATABASE_MODULE + +#include <odb/forward.hxx> // database + +#include <brep/types> +#include <brep/utility> + +#include <mod/module> +#include <mod/options> + +namespace brep +{ + // A module that utilises the database. Specifically, it will retry the + // request in the face of recoverable database failures (deadlock, loss of + // connection, etc) up to a certain number of times. + // + class database_module: public module + { + protected: + database_module () = default; + + // Create a shallow copy (handling instance) if initialized and a deep + // copy (context exemplar) otherwise. + // + explicit + database_module (const database_module&); + + // Required to avoid getting warning from clang that + // database_module::init() hides module::init() virtual functions. This + // way all functions get to the same scope and become overloaded set. + // + using module::init; + + void + init (const options::db&); + + virtual bool + handle (request&, response&) = 0; + + protected: + size_t retry_; + shared_ptr<odb::core::database> db_; + + private: + virtual bool + handle (request&, response&, log&); + }; +} + +#endif // MOD_DATABASE_MODULE diff --git a/mod/database-module.cxx b/mod/database-module.cxx new file mode 100644 index 0000000..a794672 --- /dev/null +++ b/mod/database-module.cxx @@ -0,0 +1,50 @@ +// file : mod/database-module.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/database-module> + +#include <odb/exceptions.hxx> + +#include <mod/options> +#include <mod/database> + +namespace brep +{ + // While currently the user-defined copy constructor is not required (we + // don't need to deep copy nullptr's), it is a good idea to keep the + // placeholder ready for less trivial cases. + // + database_module:: + database_module (const database_module& r) + : module (r), + retry_ (r.retry_), + db_ (r.initialized_ ? r.db_ : nullptr) + { + } + + void database_module:: + init (const options::db& o) + { + retry_ = o.db_retry (); + db_ = shared_database (o); + } + + bool database_module:: + handle (request& rq, response& rs, log& l) + try + { + return module::handle (rq, rs, l); + } + catch (const odb::recoverable& e) + { + if (retry_-- > 0) + { + MODULE_DIAG; + l1 ([&]{trace << e.what () << "; " << retry_ + 1 << " retries left";}); + throw retry (); + } + + throw; + } +} diff --git a/mod/database.cxx b/mod/database.cxx new file mode 100644 index 0000000..0f0b703 --- /dev/null +++ b/mod/database.cxx @@ -0,0 +1,60 @@ +// file : mod/database.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/database> + +#include <map> + +#include <odb/pgsql/database.hxx> +#include <odb/pgsql/connection-factory.hxx> + +namespace brep +{ + namespace options + { + bool + operator< (const db& x, const db& y) + { + int r; + if ((r = x.db_user ().compare (y.db_user ())) != 0 || + (r = x.db_password ().compare (y.db_password ())) != 0 || + (r = x.db_name ().compare (y.db_name ())) != 0 || + (r = x.db_host ().compare (y.db_host ()))) + return r < 0; + + return x.db_port () < y.db_port (); + } + } + + using namespace odb; + + shared_ptr<database> + shared_database (const options::db& o) + { + static std::map<options::db, weak_ptr<database>> databases; + + auto i (databases.find (o)); + if (i != databases.end ()) + { + if (shared_ptr<database> d = i->second.lock ()) + return d; + } + + unique_ptr<pgsql::connection_factory> + f (new pgsql::connection_pool_factory (o.db_max_connections ())); + + shared_ptr<database> d ( + make_shared<pgsql::database> ( + o.db_user (), + o.db_password (), + o.db_name (), + o.db_host (), + o.db_port (), + "options='-c default_transaction_isolation=serializable'", + move (f))); + + databases[o] = d; + return d; + } +} diff --git a/mod/diagnostics b/mod/diagnostics new file mode 100644 index 0000000..38286d4 --- /dev/null +++ b/mod/diagnostics @@ -0,0 +1,306 @@ +// file : mod/diagnostics -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_DIAGNOSTICS +#define MOD_DIAGNOSTICS + +#include <sstream> + +#include <brep/types> +#include <brep/utility> + +namespace brep +{ + struct location + { + location (): line (0), column (0) {} + location (string f, uint64_t l, uint64_t c) + : file (move (f)), line (l), column (c) {} + + string file; + uint64_t line; + uint64_t column; + }; + + enum class severity {error, warning, info, trace}; + + struct diag_entry + { + severity sev; + const char* name {nullptr}; // E.g., a function name in tracing. + location loc; + string msg; + }; + + using diag_data = vector<diag_entry>; + + // + // + template <typename> struct diag_prologue; + template <typename> struct diag_mark; + + using diag_epilogue = function<void (diag_data&&)>; + + struct diag_record + { + template <typename T> + friend const diag_record& + operator<< (const diag_record& r, const T& x) + { + r.os_ << x; + return r; + } + + diag_record () = default; + + template <typename B> + explicit + diag_record (const diag_prologue<B>& p) {*this << p;} // See below. + + template <typename B> + explicit + diag_record (const diag_mark<B>& m) {*this << m;} // See below. + + ~diag_record () noexcept(false); + + void + append (const diag_epilogue& e) const + { + if (epilogue_ == nullptr) // Keep the first epilogue (think 'fail'). + epilogue_ = &e; + + if (!data_.empty ()) + { + data_.back ().msg = os_.str (); + + // Reset the stream. There got to be a more efficient way to do it. + // + os_.clear (); + os_.str (""); + } + + data_.push_back (diag_entry ()); + } + + diag_entry& + current () const {return data_.back ();} + + // Move constructible-only type. + // + /* + @@ libstdc++ doesn't yet have the ostringstream move support. + + diag_record (diag_record&& r) + : data_ (move (r.data_)), os_ (move (r.os_)) + { + epilogue_ = r.epilogue_; + r.data_.clear (); // Empty. + } + */ + + diag_record (diag_record&& r): data_ (move (r.data_)) + { + if (!data_.empty ()) + os_ << r.os_.str (); + + epilogue_ = r.epilogue_; + r.data_.clear (); // Empty. + } + + diag_record& operator= (diag_record&&) = delete; + + diag_record (const diag_record&) = delete; + diag_record& operator= (const diag_record&) = delete; + + private: + mutable diag_data data_; + mutable std::ostringstream os_; + mutable const diag_epilogue* epilogue_ {nullptr}; + }; + + // Base (B) should provide operator() that configures diag_record. + // + template <typename B> + struct diag_prologue: B + { + diag_prologue (const diag_epilogue& e): B (), epilogue_ (e) {} + + template <typename... A> + diag_prologue (const diag_epilogue& e, A&&... a) + : B (forward<A> (a)...), epilogue_ (e) {} + + template <typename T> + diag_record + operator<< (const T& x) const + { + diag_record r; + r.append (epilogue_); + B::operator() (r); + r << x; + return r; + } + + friend const diag_record& + operator<< (const diag_record& r, const diag_prologue& p) + { + r.append (p.epilogue_); + p (r); + return r; + } + + private: + const diag_epilogue& epilogue_; + }; + + // Base (B) should provide operator() that returns diag_prologue. + // + template <typename B> + struct diag_mark: B + { + diag_mark (): B () {} + + template <typename... A> + diag_mark (A&&... a): B (forward<A> (a)...) {} + + template <typename T> + diag_record + operator<< (const T& x) const + { + return B::operator() () << x; + } + + friend const diag_record& + operator<< (const diag_record& r, const diag_mark& m) + { + return r << m (); + } + }; + + // Prologues. + // + struct simple_prologue_base + { + explicit + simple_prologue_base (severity s, const char* name) + : sev_ (s), name_ (name) {} + + void + operator() (const diag_record& r) const + { + diag_entry& e (r.current ()); + e.sev = sev_; + e.name = name_; + } + + private: + severity sev_; + const char* name_; + }; + typedef diag_prologue<simple_prologue_base> simple_prologue; + + struct location_prologue_base + { + location_prologue_base (severity s, + const char* name, + const location& l) + : sev_ (s), name_ (name), loc_ (l) {} + + void + operator() (const diag_record& r) const + { + diag_entry& e (r.current ()); + e.sev = sev_; + e.name = name_; + e.loc = loc_; //@@ I think we can probably move it. + } + + private: + severity sev_; + const char* name_; + const location loc_; + }; + typedef diag_prologue<location_prologue_base> location_prologue; + + // Marks. + // + struct basic_mark_base + { + explicit + basic_mark_base (severity s, + const diag_epilogue& e, + const char* name = nullptr, + const void* data = nullptr) + : sev_ (s), epilogue_ (e), name_ (name), data_ (data) {} + + simple_prologue + operator() () const + { + return simple_prologue (epilogue_, sev_, name_); + } + + location_prologue + operator() (const location& l) const + { + return location_prologue (epilogue_, sev_, name_, l); + } + + template <typename L> + location_prologue + operator() (const L& l) const + { + // get_location() is the user-supplied ADL-searched function. + // + return location_prologue ( + epilogue_, sev_, name_, get_location (l, data_)); + } + + private: + severity sev_; + const diag_epilogue& epilogue_; + const char* name_; + const void* data_; + }; + typedef diag_mark<basic_mark_base> basic_mark; + + template <typename E> + struct fail_mark_base + { + explicit + fail_mark_base (const char* name = nullptr, const void* data = nullptr) + : name_ (name), data_ (data) {} + + simple_prologue + operator() () const + { + return simple_prologue (epilogue_, severity::error, name_); + } + + location_prologue + operator() (const location& l) const + { + return location_prologue (epilogue_, severity::error, name_, l); + } + + template <typename L> + location_prologue + operator() (const L& l) const + { + return location_prologue ( + epilogue_, severity::error, name_, get_location (l, data_)); + } + + static void + epilogue (diag_data&& d) {throw E (move (d));} + + private: + const diag_epilogue epilogue_ {&epilogue}; + const char* name_; + const void* data_; + }; + + template <typename E> + using fail_mark = diag_mark<fail_mark_base<E>>; +} + +#endif // MOD_DIAGNOSTICS diff --git a/mod/diagnostics.cxx b/mod/diagnostics.cxx new file mode 100644 index 0000000..6512517 --- /dev/null +++ b/mod/diagnostics.cxx @@ -0,0 +1,30 @@ +// file : mod/diagnostics.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/diagnostics> + +using namespace std; + +namespace brep +{ + diag_record:: + ~diag_record () noexcept(false) + { + // Don't flush the record if this destructor was called as part of + // the stack unwinding. Right now this means we cannot use this + // mechanism in destructors, which is not a big deal, except for + // one place: exception_guard. So for now we are going to have + // this ugly special check which we will be able to get rid of + // once C++17 uncaught_exceptions() becomes available. + // + if (!data_.empty () && + (!uncaught_exception () /*|| exception_unwinding_dtor*/)) + { + data_.back ().msg = os_.str (); // Save last message. + + assert (epilogue_ != nullptr); + (*epilogue_) (move (data_)); // Can throw. + } + } +} diff --git a/mod/mod-package-details b/mod/mod-package-details new file mode 100644 index 0000000..b324bfb --- /dev/null +++ b/mod/mod-package-details @@ -0,0 +1,42 @@ +// file : mod/mod-package-details -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_MOD_PACKAGE_DETAILS +#define MOD_MOD_PACKAGE_DETAILS + +#include <brep/types> +#include <brep/utility> + +#include <mod/options> +#include <mod/database-module> + +namespace brep +{ + class package_details: public database_module + { + public: + package_details () = default; + + // Create a shallow copy (handling instance) if initialized and a deep + // copy (context exemplar) otherwise. + // + explicit + package_details (const package_details&); + + virtual bool + handle (request&, response&); + + virtual const cli::options& + cli_options () const {return options::package_details::description ();} + + private: + virtual void + init (cli::scanner&); + + private: + shared_ptr<options::package_details> options_; + }; +} + +#endif // MOD_MOD_PACKAGE_DETAILS diff --git a/mod/mod-package-details.cxx b/mod/mod-package-details.cxx new file mode 100644 index 0000000..2ff93c9 --- /dev/null +++ b/mod/mod-package-details.cxx @@ -0,0 +1,258 @@ +// file : mod/mod-package-details.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/mod-package-details> + +#include <xml/serializer> + +#include <odb/session.hxx> +#include <odb/database.hxx> +#include <odb/transaction.hxx> + +#include <web/xhtml> +#include <web/module> +#include <web/xhtml-fragment> +#include <web/mime-url-encoding> + +#include <brep/package> +#include <brep/package-odb> + +#include <mod/page> +#include <mod/options> + +using namespace odb::core; +using namespace brep::cli; + +// While currently the user-defined copy constructor is not required (we don't +// need to deep copy nullptr's), it is a good idea to keep the placeholder +// ready for less trivial cases. +// +brep::package_details:: +package_details (const package_details& r) + : database_module (r), + options_ (r.initialized_ ? r.options_ : nullptr) +{ +} + +void brep::package_details:: +init (scanner& s) +{ + MODULE_DIAG; + + options_ = make_shared<options::package_details> ( + s, unknown_mode::fail, unknown_mode::fail); + + database_module::init (*options_); + + if (options_->root ().empty ()) + options_->root (dir_path ("/")); +} + +template <typename T> +static inline query<T> +search_params (const brep::string& n, const brep::string& q) +{ + using query = query<T>; + + return "(" + + (q.empty () + ? query ("NULL") + : "plainto_tsquery (" + query::_val (q) + ")") + + "," + + query::_val (n) + + ")"; +} + +bool brep::package_details:: +handle (request& rq, response& rs) +{ + using namespace web; + using namespace web::xhtml; + + MODULE_DIAG; + + const size_t res_page (options_->search_results ()); + const dir_path& root (options_->root ()); + + const string& name (*rq.path ().rbegin ()); + const string ename (mime_url_encode (name)); + + params::package_details params; + bool full; + + try + { + name_value_scanner s (rq.parameters ()); + params = params::package_details ( + s, unknown_mode::fail, unknown_mode::fail); + + full = params.form () == page_form::full; + } + catch (const cli::exception& e) + { + throw invalid_request (400, e.what ()); + } + + size_t page (params.page ()); + const string& squery (params.query ()); + + auto url ( + [&ename](bool f = false, + const string& q = "", + size_t p = 0, + const string& a = "") -> string + { + string s ("?"); + string u (ename); + + if (f) { u += "?f=full"; s = "&"; } + if (!q.empty ()) { u += s + "q=" + mime_url_encode (q); s = "&"; } + if (p > 0) { u += s + "p=" + to_string (p); s = "&"; } + if (!a.empty ()) { u += '#' + a; } + return u; + }); + + xml::serializer s (rs.content (), name); + + s << HTML + << HEAD + << TITLE + << name; + + if (!squery.empty ()) + s << " " << squery; + + s << ~TITLE + << CSS_LINKS (path ("package-details.css"), root) + // + // This hack is required to avoid the "flash of unstyled content", which + // happens due to the presence of the autofocus attribute in the input + // element of the search form. The problem appears in Firefox and has a + // (4-year old, at the time of this writing) bug report: + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=712130. + // + << SCRIPT << " " << ~SCRIPT + << ~HEAD + << BODY + << DIV_HEADER (root, options_->logo (), options_->menu ()) + << DIV(ID="content"); + + if (full) + s << CLASS("full"); + + s << DIV(ID="heading") + << H1 << A(HREF=url ()) << name << ~A << ~H1 + << A(HREF=url (!full, squery, page)) + << (full ? "[brief]" : "[full]") + << ~A + << ~DIV; + + session sn; + transaction t (db_->begin ()); + + shared_ptr<package> pkg; + { + latest_package lp; + if (!db_->query_one<latest_package> ( + query<latest_package>( + "(" + query<latest_package>::_val (name) + ")"), lp)) + throw invalid_request (404, "Package '" + name + "' not found"); + + pkg = db_->load<package> (lp.id); + } + + const auto& licenses (pkg->license_alternatives); + + if (page == 0) + { + // Display package details on the first page only. + // + s << H2 << pkg->summary << ~H2; + + static const string id ("description"); + if (const auto& d = pkg->description) + s << (full + ? P_DESCRIPTION (*d, id) + : P_DESCRIPTION (*d, options_->package_description (), + url (!full, squery, page, id))); + + s << TABLE(CLASS="proplist", ID="package") + << TBODY + << TR_LICENSE (licenses) + << TR_URL (pkg->url) + << TR_EMAIL (pkg->email) + << TR_TAGS (pkg->tags, root) + << ~TBODY + << ~TABLE; + } + + auto pkg_count ( + db_->query_value<package_count> ( + search_params<package_count> (name, squery))); + + s << FORM_SEARCH (squery) + << DIV_COUNTER (pkg_count, "Version", "Versions"); + + // Enclose the subsequent tables to be able to use nth-child CSS selector. + // + s << DIV; + for (const auto& pr: + db_->query<package_search_rank> ( + search_params<package_search_rank> (name, squery) + + "ORDER BY rank DESC, version_epoch DESC, " + "version_canonical_upstream DESC, version_canonical_release DESC, " + "version_revision DESC" + + "OFFSET" + to_string (page * res_page) + + "LIMIT" + to_string (res_page))) + { + shared_ptr<package> p (db_->load<package> (pr.id)); + + s << TABLE(CLASS="proplist version") + << TBODY + << TR_VERSION (name, p->version.string (), root) + + // @@ Shouldn't we skip low priority row ? Don't think so, why? + // + << TR_PRIORITY (p->priority); + + // Comparing objects of the license_alternatives class as being of the + // vector<vector<string>> class, so comments are not considered. + // + if (p->license_alternatives != licenses) + s << TR_LICENSE (p->license_alternatives); + + assert (p->internal ()); + + // @@ Shouldn't we make package location to be a link to the proper + // place of the About page, describing corresponding repository? + // Yes, I think that's sounds reasonable, once we have about. + // Or maybe it can be something more valuable like a link to the + // repository package search page ? + // + // @@ In most cases package location will be the same for all versions + // of the same package. Shouldn't we put package location to the + // package summary part and display it here only if it differs + // from the one in the summary ? + // + // Hm, I am not so sure about this. Consider: stable/testing/unstable. + // + s << TR_LOCATION (p->internal_repository.object_id (), root) + << TR_DEPENDS (p->dependencies, root) + << TR_REQUIRES (p->requirements) + << ~TBODY + << ~TABLE; + } + s << ~DIV; + + t.commit (); + + s << DIV_PAGER (page, pkg_count, res_page, options_->search_pages (), + url (full, squery)) + << ~DIV + << ~BODY + << ~HTML; + + return true; +} diff --git a/mod/mod-package-search b/mod/mod-package-search new file mode 100644 index 0000000..e463e2a --- /dev/null +++ b/mod/mod-package-search @@ -0,0 +1,42 @@ +// file : mod/mod-package-search -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_MOD_PACKAGE_SEARCH +#define MOD_MOD_PACKAGE_SEARCH + +#include <brep/types> +#include <brep/utility> + +#include <mod/options> +#include <mod/database-module> + +namespace brep +{ + class package_search: public database_module + { + public: + package_search () = default; + + // Create a shallow copy (handling instance) if initialized and a deep + // copy (context exemplar) otherwise. + // + explicit + package_search (const package_search&); + + virtual bool + handle (request&, response&); + + virtual const cli::options& + cli_options () const {return options::package_search::description ();} + + private: + virtual void + init (cli::scanner&); + + private: + shared_ptr<options::package_search> options_; + }; +} + +#endif // MOD_MOD_PACKAGE_SEARCH diff --git a/mod/mod-package-search.cxx b/mod/mod-package-search.cxx new file mode 100644 index 0000000..5654427 --- /dev/null +++ b/mod/mod-package-search.cxx @@ -0,0 +1,181 @@ +// file : mod/mod-package-search.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/mod-package-search> + +#include <xml/serializer> + +#include <odb/session.hxx> +#include <odb/database.hxx> +#include <odb/transaction.hxx> +#include <odb/schema-catalog.hxx> + +#include <web/xhtml> +#include <web/module> +#include <web/xhtml-fragment> +#include <web/mime-url-encoding> + +#include <brep/version> +#include <brep/package> +#include <brep/package-odb> + +#include <mod/page> +#include <mod/options> + +using namespace odb::core; +using namespace brep::cli; + +// While currently the user-defined copy constructor is not required (we don't +// need to deep copy nullptr's), it is a good idea to keep the placeholder +// ready for less trivial cases. +// +brep::package_search:: +package_search (const package_search& r) + : database_module (r), + options_ (r.initialized_ ? r.options_ : nullptr) +{ +} + +void brep::package_search:: +init (scanner& s) +{ + MODULE_DIAG; + + options_ = make_shared<options::package_search> ( + s, unknown_mode::fail, unknown_mode::fail); + + database_module::init (*options_); + + if (options_->root ().empty ()) + options_->root (dir_path ("/")); + + // Check that the database schema matches the current one. It's enough to + // perform the check in just a single module implementation (and we don't + // do in the dispatcher because it doesn't use the database). + // + // Note that the failure can be reported by each web server worker process. + // While it could be tempting to move the check to the + // repository_root::version() function, it would be wrong. The function can + // be called by a different process (usually the web server root one) not + // having the proper permissions to access the database. + // + if (schema_catalog::current_version (*db_) != db_->schema_version ()) + fail << "database schema differs from the current one (module " + << BREP_VERSION_STR << ")"; +} + +template <typename T> +static inline query<T> +search_param (const brep::string& q) +{ + using query = query<T>; + return "(" + + (q.empty () + ? query ("NULL") + : "plainto_tsquery (" + query::_val (q) + ")") + + ")"; +} + +bool brep::package_search:: +handle (request& rq, response& rs) +{ + using namespace web::xhtml; + + MODULE_DIAG; + + const size_t res_page (options_->search_results ()); + const dir_path& root (options_->root ()); + + params::package_search params; + + try + { + name_value_scanner s (rq.parameters ()); + params = params::package_search ( + s, unknown_mode::fail, unknown_mode::fail); + } + catch (const unknown_argument& e) + { + throw invalid_request (400, e.what ()); + } + + size_t page (params.page ()); + const string& squery (params.query ()); + string squery_param (squery.empty () + ? "" + : "?q=" + web::mime_url_encode (squery)); + + + static const string title ("Packages"); + xml::serializer s (rs.content (), title); + + s << HTML + << HEAD + << TITLE + << title; + + if (!squery.empty ()) + s << " " << squery; + + s << ~TITLE + << CSS_LINKS (path ("package-search.css"), root) + // + // This hack is required to avoid the "flash of unstyled content", which + // happens due to the presence of the autofocus attribute in the input + // element of the search form. The problem appears in Firefox and has a + // (4-year old, at the time of this writing) bug report: + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=712130. + // + << SCRIPT << " " << ~SCRIPT + << ~HEAD + << BODY + << DIV_HEADER (root, options_->logo (), options_->menu ()) + << DIV(ID="content"); + + session sn; + transaction t (db_->begin ()); + + auto pkg_count ( + db_->query_value<latest_package_count> ( + search_param<latest_package_count> (squery))); + + s << FORM_SEARCH (squery) + << DIV_COUNTER (pkg_count, "Package", "Packages"); + + // Enclose the subsequent tables to be able to use nth-child CSS selector. + // + s << DIV; + for (const auto& pr: + db_->query<latest_package_search_rank> ( + search_param<latest_package_search_rank> (squery) + + "ORDER BY rank DESC, name" + + "OFFSET" + to_string (page * res_page) + + "LIMIT" + to_string (res_page))) + { + shared_ptr<package> p (db_->load<package> (pr.id)); + + s << TABLE(CLASS="proplist package") + << TBODY + << TR_NAME (p->id.name, squery_param, root) + << TR_SUMMARY (p->summary) + << TR_LICENSE (p->license_alternatives) + << TR_TAGS (p->tags, root) + << TR_DEPENDS (p->dependencies, root) + << TR_REQUIRES (p->requirements) + << ~TBODY + << ~TABLE; + } + s << ~DIV; + + t.commit (); + + s << DIV_PAGER (page, pkg_count, res_page, options_->search_pages (), + root.string () + squery_param) + << ~DIV + << ~BODY + << ~HTML; + + return true; +} diff --git a/mod/mod-package-version-details b/mod/mod-package-version-details new file mode 100644 index 0000000..0fba2bf --- /dev/null +++ b/mod/mod-package-version-details @@ -0,0 +1,45 @@ +// file : mod/mod-package-version-details -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_MOD_PACKAGE_VERSION_DETAILS +#define MOD_MOD_PACKAGE_VERSION_DETAILS + +#include <brep/types> +#include <brep/utility> + +#include <mod/options> +#include <mod/database-module> + +namespace brep +{ + class package_version_details: public database_module + { + public: + package_version_details () = default; + + // Create a shallow copy (handling instance) if initialized and a deep + // copy (context exemplar) otherwise. + // + explicit + package_version_details (const package_version_details&); + + virtual bool + handle (request&, response&); + + virtual const cli::options& + cli_options () const + { + return options::package_version_details::description (); + } + + private: + virtual void + init (cli::scanner&); + + private: + shared_ptr<options::package_version_details> options_; + }; +} + +#endif // MOD_MOD_PACKAGE_VERSION_DETAILS diff --git a/mod/mod-package-version-details.cxx b/mod/mod-package-version-details.cxx new file mode 100644 index 0000000..149c8f9 --- /dev/null +++ b/mod/mod-package-version-details.cxx @@ -0,0 +1,320 @@ +// file : mod/mod-package-version-details.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/mod-package-version-details> + +#include <xml/serializer> + +#include <odb/session.hxx> +#include <odb/database.hxx> +#include <odb/transaction.hxx> + +#include <web/xhtml> +#include <web/module> +#include <web/xhtml-fragment> +#include <web/mime-url-encoding> + +#include <brep/package> +#include <brep/package-odb> + +#include <mod/page> +#include <mod/options> + +using namespace std; +using namespace odb::core; +using namespace brep::cli; + +// While currently the user-defined copy constructor is not required (we don't +// need to deep copy nullptr's), it is a good idea to keep the placeholder +// ready for less trivial cases. +// +brep::package_version_details:: +package_version_details (const package_version_details& r) + : database_module (r), + options_ (r.initialized_ ? r.options_ : nullptr) +{ +} + +void brep::package_version_details:: +init (scanner& s) +{ + MODULE_DIAG; + + options_ = make_shared<options::package_version_details> ( + s, unknown_mode::fail, unknown_mode::fail); + + database_module::init (*options_); + + if (options_->root ().empty ()) + options_->root (dir_path ("/")); +} + +bool brep::package_version_details:: +handle (request& rq, response& rs) +{ + using namespace web; + using namespace web::xhtml; + using brep::version; // Not to confuse with module::version. + + MODULE_DIAG; + + const dir_path& root (options_->root ()); + + auto i (rq.path ().rbegin ()); + version ver; + + try + { + ver = version (*i++); + } + catch (const invalid_argument& ) + { + throw invalid_request (400, "invalid package version format"); + } + + const string& sver (ver.string ()); + + assert (i != rq.path ().rend ()); + const string& name (*i); + + params::package_version_details params; + bool full; + + try + { + name_value_scanner s (rq.parameters ()); + params = params::package_version_details ( + s, unknown_mode::fail, unknown_mode::fail); + + full = params.form () == page_form::full; + } + catch (const unknown_argument& e) + { + throw invalid_request (400, e.what ()); + } + + auto url ( + [&sver](bool f = false, const string& a = "") -> string + { + string u (sver); + + if (f) { u += "?f=full"; } + if (!a.empty ()) { u += '#' + a; } + return u; + }); + + const string title (name + " " + sver); + xml::serializer s (rs.content (), title); + + s << HTML + << HEAD + << TITLE << title << ~TITLE + << CSS_LINKS (path ("package-version-details.css"), root) + << ~HEAD + << BODY + << DIV_HEADER (root, options_->logo (), options_->menu ()) + << DIV(ID="content"); + + if (full) + s << CLASS("full"); + + s << DIV(ID="heading") + << H1 + << A(HREF=root / path (mime_url_encode (name))) << name << ~A + << "/" + << A(HREF=url ()) << sver << ~A + << ~H1 + << A(HREF=url (!full)) << (full ? "[brief]" : "[full]") << ~A + << ~DIV; + + bool not_found (false); + shared_ptr<package> pkg; + + session sn; + transaction t (db_->begin ()); + + try + { + pkg = db_->load<package> (package_id (name, ver)); + + // If the requested package turned up to be an "external" one just + // respond that no "internal" package is present. + // + not_found = !pkg->internal (); + } + catch (const object_not_persistent& ) + { + not_found = true; + } + + if (not_found) + throw invalid_request (404, "Package '" + title + "' not found"); + + s << H2 << pkg->summary << ~H2; + + static const string id ("description"); + if (const auto& d = pkg->description) + s << (full + ? P_DESCRIPTION (*d, id) + : P_DESCRIPTION (*d, options_->package_description (), + url (!full, id))); + + assert (pkg->location && pkg->sha256sum); + + s << TABLE(CLASS="proplist", ID="version") + << TBODY + + // Repeat version here since it can be cut out in the header. + // + << TR_VERSION (pkg->version.string ()) + + << TR_PRIORITY (pkg->priority) + << TR_LICENSES (pkg->license_alternatives) + << TR_LOCATION (pkg->internal_repository.object_id (), root) + << TR_DOWNLOAD (pkg->internal_repository.load ()->location.string () + + "/" + pkg->location->string ()) + << TR_SHA256SUM (*pkg->sha256sum) + << ~TBODY + << ~TABLE + + << TABLE(CLASS="proplist", ID="package") + << TBODY + << TR_URL (pkg->url) + << TR_EMAIL (pkg->email); + + const auto& pu (pkg->package_url); + if (pu && *pu != pkg->url) + s << TR_URL (*pu, "pkg-url"); + + const auto& pe (pkg->package_email); + if (pe && *pe != pkg->email) + s << TR_EMAIL (*pe, "pkg-email"); + + s << TR_TAGS (pkg->tags, root) + << ~TBODY + << ~TABLE; + + const auto& ds (pkg->dependencies); + if (!ds.empty ()) + { + s << H3 << "Depends" << ~H3 + << TABLE(CLASS="proplist", ID="depends") + << TBODY; + + for (const auto& da: ds) + { + s << TR(CLASS="depends") + << TH; + + if (da.conditional) + s << "?"; + + s << ~TH + << TD + << SPAN(CLASS="value"); + + for (const auto& d: da) + { + if (&d != &da[0]) + s << " | "; + + shared_ptr<package> p (d.package.load ()); + assert (p->internal () || !p->other_repositories.empty ()); + + shared_ptr<repository> r ( + p->internal () + ? p->internal_repository.load () + : p->other_repositories[0].load ()); + + const auto& dcon (d.constraint); + const string& dname (p->id.name); + string ename (mime_url_encode (dname)); + + if (r->url) + { + string u (*r->url + ename); + s << A(HREF=u) << dname << ~A; + + if (dcon) + s << ' ' << A(HREF=u + "/" + p->version.string ()) << *dcon << ~A; + } + else if (p->internal ()) + { + path u (root / path (ename)); + s << A(HREF=u) << dname << ~A; + + if (dcon) + s << ' ' << A(HREF=u / path (p->version.string ())) << *dcon << ~A; + } + else + // Display the dependency as a plain text if no repository URL + // available. + // + s << d; + } + + s << ~SPAN + << SPAN_COMMENT (da.comment) + << ~TD + << ~TR; + } + + s << ~TBODY + << ~TABLE; + } + + t.commit (); + + const auto& rm (pkg->requirements); + if (!rm.empty ()) + { + s << H3 << "Requires" << ~H3 + << TABLE(CLASS="proplist", ID="requires") + << TBODY; + + for (const auto& ra: rm) + { + s << TR(CLASS="requires") + << TH; + + if (ra.conditional) + s << "?"; + + s << ~TH + << TD + << SPAN(CLASS="value"); + + for (const auto& r: ra) + { + if (&r != &ra[0]) + s << " | "; + + s << r; + } + + s << ~SPAN + << SPAN_COMMENT (ra.comment) + << ~TD + << ~TR; + } + + s << ~TBODY + << ~TABLE; + } + + const auto& ch (pkg->changes); + if (!ch.empty ()) + s << H3 << "Changes" << ~H3 + << (full + ? PRE_CHANGES (ch) + : PRE_CHANGES (ch, + options_->package_changes (), + url (!full, "changes"))); + + s << ~DIV + << ~BODY + << ~HTML; + + return true; +} diff --git a/mod/mod-repository-details b/mod/mod-repository-details new file mode 100644 index 0000000..2532613 --- /dev/null +++ b/mod/mod-repository-details @@ -0,0 +1,42 @@ +// file : mod/mod-repository-details -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_MOD_REPOSITORY_DETAILS +#define MOD_MOD_REPOSITORY_DETAILS + +#include <brep/types> +#include <brep/utility> + +#include <mod/options> +#include <mod/database-module> + +namespace brep +{ + class repository_details: public database_module + { + public: + repository_details () = default; + + // Create a shallow copy (handling instance) if initialized and a deep + // copy (context exemplar) otherwise. + // + explicit + repository_details (const repository_details&); + + virtual bool + handle (request&, response&); + + virtual const cli::options& + cli_options () const {return options::repository_details::description ();} + + private: + virtual void + init (cli::scanner&); + + private: + shared_ptr<options::repository_details> options_; + }; +} + +#endif // MOD_MOD_REPOSITORY_DETAILS diff --git a/mod/mod-repository-details.cxx b/mod/mod-repository-details.cxx new file mode 100644 index 0000000..91f0d1b --- /dev/null +++ b/mod/mod-repository-details.cxx @@ -0,0 +1,147 @@ +// file : mod/mod-repository-details.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/mod-repository-details> + +#include <time.h> // tzset() + +#include <sstream> +#include <algorithm> // max() + +#include <xml/serializer> + +#include <odb/database.hxx> +#include <odb/transaction.hxx> + +#include <butl/timestamp> + +#include <web/xhtml> +#include <web/module> +#include <web/xhtml-fragment> +#include <web/mime-url-encoding> + +#include <brep/package> +#include <brep/package-odb> + +#include <mod/page> +#include <mod/options> + +using namespace std; +using namespace odb::core; +using namespace brep::cli; + +// While currently the user-defined copy constructor is not required (we don't +// need to deep copy nullptr's), it is a good idea to keep the placeholder +// ready for less trivial cases. +// +brep::repository_details:: +repository_details (const repository_details& r) + : database_module (r), + options_ (r.initialized_ ? r.options_ : nullptr) +{ +} + +void brep::repository_details:: +init (scanner& s) +{ + MODULE_DIAG; + + options_ = make_shared<options::repository_details> ( + s, unknown_mode::fail, unknown_mode::fail); + + database_module::init (*options_); + + if (options_->root ().empty ()) + options_->root (dir_path ("/")); + + tzset (); // To use butl::to_stream() later on. +} + +bool brep::repository_details:: +handle (request& rq, response& rs) +{ + using namespace web::xhtml; + + MODULE_DIAG; + + const dir_path& root (options_->root ()); + + // Make sure no parameters passed. + // + try + { + name_value_scanner s (rq.parameters ()); + params::repository_details (s, unknown_mode::fail, unknown_mode::fail); + } + catch (const unknown_argument& e) + { + throw invalid_request (400, e.what ()); + } + + static const string title ("About"); + xml::serializer s (rs.content (), title); + + s << HTML + << HEAD + << TITLE << title << ~TITLE + << CSS_LINKS (path ("repository-details.css"), root) + << ~HEAD + << BODY + << DIV_HEADER (root, options_->logo (), options_->menu ()) + << DIV(ID="content"); + + transaction t (db_->begin ()); + + using query = query<repository>; + + for (const auto& r: + db_->query<repository> ( + query::internal + "ORDER BY" + query::priority)) + { + //@@ Feels like a lot of trouble (e.g., id_attribute()) for very + // dubious value. A link to the package search page just for + // this repository would probably be more useful. + // + string id (html_id (r.name)); + s << H1(ID=id) + << A(HREF="#" + web::mime_url_encode (id)) << r.display_name << ~A + << ~H1; + + if (r.summary) + s << H2 << *r.summary << ~H2; + + if (r.description) + s << P_DESCRIPTION (*r.description); + + if (r.email) + { + const email& e (*r.email); + + s << P + << A(HREF="mailto:" + e) << e << ~A; + + if (!e.comment.empty ()) + s << " (" << e.comment << ")"; + + s << ~P; + } + + ostringstream o; + butl::to_stream (o, + max (r.packages_timestamp, r.repositories_timestamp), + "%Y-%m-%d %H:%M:%S%[.N] %Z", + true, + true); + + s << P << o.str () << ~P; + } + + t.commit (); + + s << ~DIV + << ~BODY + << ~HTML; + + return true; +} diff --git a/mod/mod-repository-root b/mod/mod-repository-root new file mode 100644 index 0000000..db13b58 --- /dev/null +++ b/mod/mod-repository-root @@ -0,0 +1,60 @@ +// file : mod/mod-repository-root -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_MOD_REPOSITORY_ROOT +#define MOD_MOD_REPOSITORY_ROOT + +#include <brep/types> +#include <brep/utility> + +#include <mod/module> +#include <mod/options> + +namespace brep +{ + class package_search; + class package_details; + class package_version_details; + class repository_details; + + class repository_root: public module + { + public: + repository_root (); + + // Create a shallow copy (handling instance) if initialized and a deep + // copy (context exemplar) otherwise. + // + explicit + repository_root (const repository_root&); + + private: + virtual bool + handle (request&, response&); + + virtual const cli::options& + cli_options () const {return options::repository_root::description ();} + + virtual option_descriptions + options (); + + virtual void + init (const name_values&); + + virtual void + init (cli::scanner&); + + virtual void + version (); + + private: + shared_ptr<package_search> package_search_; + shared_ptr<package_details> package_details_; + shared_ptr<package_version_details> package_version_details_; + shared_ptr<repository_details> repository_details_; + shared_ptr<options::repository_root> options_; + }; +} + +#endif // MOD_MOD_REPOSITORY_ROOT diff --git a/mod/mod-repository-root.cxx b/mod/mod-repository-root.cxx new file mode 100644 index 0000000..295117e --- /dev/null +++ b/mod/mod-repository-root.cxx @@ -0,0 +1,263 @@ +// file : mod/mod-repository-root.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/mod-repository-root> + +#include <sstream> + +#include <web/module> + +#include <brep/version> + +#include <mod/module> +#include <mod/options> +#include <mod/mod-package-search> +#include <mod/mod-package-details> +#include <mod/mod-repository-details> +#include <mod/mod-package-version-details> + +using namespace std; +using namespace brep::cli; + +namespace brep +{ + // request_proxy + // + class request_proxy: public request + { + public: + request_proxy (request& r, const name_values& p) + : request_ (r), parameters_ (p) {} + + virtual const path_type& + path () {return request_.path ();} + + virtual const name_values& + parameters () {return parameters_;} + + virtual const name_values& + cookies () {return request_.cookies ();} + + virtual istream& + content (bool buffer) {return request_.content (buffer);} + + private: + request& request_; + const name_values& parameters_; + }; + + // repository_root + // + repository_root:: + repository_root () + : package_search_ (make_shared<package_search> ()), + package_details_ (make_shared<package_details> ()), + package_version_details_ (make_shared<package_version_details> ()), + repository_details_ (make_shared<repository_details> ()) + { + } + + repository_root:: + repository_root (const repository_root& r) + : module (r), + // + // Deep/shallow-copy sub-modules depending on whether this is an + // exemplar/handler. + // + package_search_ ( + r.initialized_ + ? r.package_search_ + : make_shared<package_search> (*r.package_search_)), + package_details_ ( + r.initialized_ + ? r.package_details_ + : make_shared<package_details> (*r.package_details_)), + package_version_details_ ( + r.initialized_ + ? r.package_version_details_ + : make_shared<package_version_details> ( + *r.package_version_details_)), + repository_details_ ( + r.initialized_ + ? r.repository_details_ + : make_shared<repository_details> (*r.repository_details_)), + options_ ( + r.initialized_ + ? r.options_ + : nullptr) + { + } + + // Return amalgamation of repository_root and all its sub-modules option + // descriptions. + // + option_descriptions repository_root:: + options () + { + option_descriptions r (module::options ()); + append (r, package_search_->options ()); + append (r, package_details_->options ()); + append (r, package_version_details_->options ()); + append (r, repository_details_->options ()); + return r; + } + + // Initialize sub-modules and parse own configuration options. + // + void repository_root:: + init (const name_values& v) + { + auto sub_init ([this, &v](module& m) + { + m.init (filter (v, m.options ()), *log_); + }); + + // Initialize sub-modules. + // + sub_init (*package_search_); + sub_init (*package_details_); + sub_init (*package_version_details_); + sub_init (*repository_details_); + + // Parse own configuration options. + // + module::init ( + filter (v, convert (options::repository_root::description ()))); + } + + void repository_root:: + init (scanner& s) + { + MODULE_DIAG; + + options_ = make_shared<options::repository_root> ( + s, unknown_mode::fail, unknown_mode::fail); + + if (options_->root ().empty ()) + options_->root (dir_path ("/")); + } + + bool repository_root:: + handle (request& rq, response& rs) + { + MODULE_DIAG; + + const dir_path& root (options_->root ()); + + const path& rpath (rq.path ()); + if (!rpath.sub (root)) + return false; + + const path& lpath (rpath.leaf (root)); + + // Delegate the request handling to the sub-module. Intercept exception + // handling to add sub-module attribution. + // + auto handle = [&rs, this](module& m, request& rq, const char* name) -> bool + { + try + { + return m.handle (rq, rs, *log_); + } + catch (const invalid_request&) + { + // Preserve invalid_request exception type, so the web server can + // properly respond to the client with a 4XX error code. + // + throw; + } + catch (const std::exception& e) + { + // All exception types inherited from std::exception (and different + // from invalid_request) are handled by the web server as + // std::exception. The only sensible way to handle them is to respond + // to the client with the internal server error (500) code. By that + // reason it is valid to reduce all these types to a single one. + // Note that the server_error exception is handled internally by the + // module::handle() function call. + // + throw runtime_error (string (name) + ": " + e.what ()); + } + }; + + if (lpath.empty ()) + { + // Dispatch request handling to the repository_details or the + // package_search module depending on the function name passed as a + // first HTTP request parameter. The parameter should have no value + // specified. Example: cppget.org/?about + // + const name_values& params (rq.parameters ()); + if (!params.empty () && !params.front ().value) + { + if (params.front ().name == "about") + { + // Cleanup not to confuse the selected module with the unknown + // parameter. + // + name_values p (params); + p.erase (p.begin ()); + + request_proxy rp (rq, p); + repository_details m (*repository_details_); + return handle (m, rp, "repository_details"); + } + + throw invalid_request (400, "unknown function"); + } + else + { + package_search m (*package_search_); + return handle (m, rq, "package_search"); + } + } + else + { + // Dispatch request handling to the package_details or the + // package_version_details module depending on the HTTP request URL path. + // + auto i (lpath.begin ()); + assert (i != lpath.end ()); + + const string& n (*i++); // Package name. + + // Check if this is a package name and not a brep static content files + // (CSS) directory name, a repository directory name, or a special file + // name (the one starting with '.'). + // + // @@ Shouldn't we validate that the package name is not "@", is not + // digit-only, does not start with '.' while parsing and serializing + // the package manifest ? Probably also need to mention these + // contraints in the manifest.txt file. + // + if (n != "@" && n.find_first_not_of ("0123456789") != string::npos && + n[0] != '.') + { + if (i == lpath.end ()) + { + package_details m (*package_details_); + return handle (m, rq, "package_details"); + } + else if (++i == lpath.end ()) + { + package_version_details m (*package_version_details_); + return handle (m, rq, "package_version_details"); + } + } + } + + return false; + } + + void repository_root:: + version () + { + MODULE_DIAG; + + info << "module " << BREP_VERSION_STR + << ", libbrep " << LIBBREP_VERSION_STR + << ", libbpkg " << LIBBPKG_VERSION_STR + << ", libbutl " << LIBBUTL_VERSION_STR; + } +} diff --git a/mod/module b/mod/module new file mode 100644 index 0000000..b704a53 --- /dev/null +++ b/mod/module @@ -0,0 +1,201 @@ +// file : mod/module -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_MODULE +#define MOD_MODULE + +#include <web/module> + +#include <brep/types> +#include <brep/utility> + +#include <mod/options> +#include <mod/diagnostics> + +namespace brep +{ + // Bring in commonly used names from the web namespace. + // + // @@ Maybe doing using namespace is the right way to handle this. + // There will, however, most likely be a conflict between + // web::module and our module. Or maybe not, need to try. + // + using web::status_code; + using web::invalid_request; + using web::sequence_error; + using web::option_descriptions; + using web::name_value; + using web::name_values; + using web::request; + using web::response; + using web::log; + + // This exception indicated a server error (5XX). In particular, + // it is thrown by the fail diagnostics stream and is caught by the + // module implementation where it is both logged as an error and + // returned to the user with the 5XX status code. + // + struct server_error + { + diag_data data; + + server_error (diag_data&& d): data (move (d)) {} + }; + + // Every module member function that needs to produce any diagnostics + // shall begin with: + // + // MODULE_DIAG; + // + // This will instantiate the fail, error, warn, info, and trace + // diagnostics streams with the function's name. + // +#define MODULE_DIAG \ + const fail_mark<server_error> fail (__PRETTY_FUNCTION__); \ + const basic_mark error (severity::error, \ + this->log_writer_, \ + __PRETTY_FUNCTION__); \ + const basic_mark warn (severity::warning, \ + this->log_writer_, \ + __PRETTY_FUNCTION__); \ + const basic_mark info (severity::info, \ + this->log_writer_, \ + __PRETTY_FUNCTION__); \ + const basic_mark trace (severity::trace, \ + this->log_writer_, \ + __PRETTY_FUNCTION__) + + // Adaptation of the web::module to our needs. + // + class module: public web::module + { + // Diagnostics. + // + protected: + // Trace verbosity level. + // + // 0 - tracing disabled. + // 1 - brief information regarding irregular situations, which not being + // an error can be of some interest. + // 2 - @@ TODO: document + // + // While uint8 is more than enough, use uint16 for the ease of printing. + // + uint16_t verb_ = 0; + + template <class F> void l1 (const F& f) const {if (verb_ >= 1) f ();} + template <class F> void l2 (const F& f) const {if (verb_ >= 2) f ();} + + // Set to true when the module is successfully initialized. + // + bool initialized_ {false}; + + // Implementation details. + // + protected: + module (); + module (const module& ); + + static name_values + filter (const name_values&, const option_descriptions&); + + static option_descriptions + convert (const cli::options&); + + static void + append (option_descriptions& dst, const cli::options& src); + + static void + append (option_descriptions& dst, const option_descriptions& src); + + // Can be used by module implementation to parse HTTP request parameters. + // + class name_value_scanner: public cli::scanner + { + public: + name_value_scanner (const name_values&) noexcept; + + virtual bool + more (); + + virtual const char* + peek (); + + virtual const char* + next (); + + virtual void + skip (); + + private: + const name_values& name_values_; + name_values::const_iterator i_; + bool name_; + }; + + public: + virtual const cli::options& + cli_options () const = 0; + + virtual void + init (cli::scanner&) = 0; + + // Can be overriden by custom request dispatcher to initialize + // sub-modules. + // + virtual void + init (const name_values&); + + virtual void + init (const name_values&, log&); + + virtual bool + handle (request&, response&) = 0; + + virtual bool + handle (request&, response&, log&); + + // web::module interface. + // + public: + // Custom request dispatcher can aggregate its own option descriptions + // with sub-modules option descriptions. In this case it should still call + // the base implementation in order to include the brep::module's options. + // + virtual option_descriptions + options (); + + private: + virtual void + version (log&); + + // Can be overriden by the module implementation to log version, etc. + // + virtual void + version () {} + + name_values + expand_options (const name_values&); + + // Diagnostics implementation details. + // + protected: + log* log_ {nullptr}; // Diagnostics backend provided by the web server. + + private: + // Extract function name from a __PRETTY_FUNCTION__. + // Throw invalid_argument if fail to parse. + // + static string + func_name (const char* pretty_name); + + void + log_write (const diag_data&) const; + + protected: + const diag_epilogue log_writer_; + }; +} + +#endif // MOD_MODULE diff --git a/mod/module.cxx b/mod/module.cxx new file mode 100644 index 0000000..5e3a4b1 --- /dev/null +++ b/mod/module.cxx @@ -0,0 +1,410 @@ +// file : mod/module.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/module> + +#include <httpd.h> +#include <http_log.h> + +#include <sstream> +#include <cstring> // strchr() +#include <functional> // bind() + +#include <web/module> +#include <web/apache/log> + +#include <mod/options> + +using namespace std; +using namespace placeholders; // For std::bind's _1, etc. + +namespace brep +{ + // module + // + bool module:: + handle (request& rq, response& rs, log& l) + { + log_ = &l; + + try + { + // Web server should terminate if initialization failed. + // + assert (initialized_); + + return handle (rq, rs); + } + catch (const server_error& e) + { + log_write (e.data); + + try + { + static const char* sev_str[] = {"error", "warning", "info", "trace"}; + ostream& o (rs.content (500, "text/plain;charset=utf-8")); + + for (const auto& d: e.data) + { + string name; + + try + { + name = func_name (d.name); + } + catch (const invalid_argument&) + { + // Log "pretty" function description, see in log file & fix. + name = d.name; + } + + o << name << ": " << sev_str[static_cast<size_t> (d.sev)] << ": " + << d.msg << endl; + } + } + catch (const sequence_error&) + { + // We tried to return the error status/description but some + // content has already been written. Nothing we can do about + // it. + } + } + + return true; + } + + option_descriptions module:: + convert (const cli::options& o) + { + option_descriptions r; + append (r, o); + return r; + } + + void module:: + append (option_descriptions& dst, const cli::options& src) + { + for (const auto& o: src) + { + bool v (!o.flag ()); + auto i (dst.emplace (o.name (), v)); + assert (i.first->second == v); // Consistent option/flag. + + for (const auto& a: o.aliases ()) + { + i = dst.emplace (a, v); + assert (i.first->second == v); + } + } + } + + void module:: + append (option_descriptions& dst, const option_descriptions& src) + { + for (const auto& o: src) + { + auto i (dst.emplace (o)); + assert (i.first->second == o.second); // Consistent option/flag. + } + } + + name_values module:: + filter (const name_values& v, const option_descriptions& d) + { + name_values r; + for (const auto& nv: v) + { + if (d.find (nv.name) != d.end ()) + r.push_back (nv); + } + + return r; + } + + // Convert CLI option descriptions to the general interface of option + // descriptions, extend with brep::module own option descriptions. + // + option_descriptions module:: + options () + { + option_descriptions r ({{"conf", true}}); + append (r, options::module::description ()); + append (r, cli_options ()); + return r; + } + + // Expand option list parsing configuration files. + // + name_values module:: + expand_options (const name_values& v) + { + using namespace cli; + + vector<const char*> argv; + for (const auto& nv: v) + { + argv.push_back (nv.name.c_str ()); + + if (nv.value) + argv.push_back (nv.value->c_str ()); + } + + int argc (argv.size ()); + argv_file_scanner s (0, argc, const_cast<char**> (argv.data ()), "conf"); + + name_values r; + const option_descriptions& o (options ()); + + while (s.more ()) + { + string n (s.next ()); + auto i (o.find (n)); + + if (i == o.end ()) + throw unknown_argument (n); + + optional<string> v; + if (i->second) + v = s.next (); + + r.emplace_back (move (n), move (v)); + } + + return r; + } + + // Parse options with a cli-generated scanner. Options verb and conf are + // recognized by brep::module::init while others to be interpreted by the + // derived init(). If there is an option which can not be interpreted + // neither by brep::module nor by the derived class, then the web server + // is terminated with a corresponding error message being logged. Though + // this should not happen if the options() function returned the correct + // set of options. + // + void module:: + init (const name_values& options, log& log) + { + assert (!initialized_); + + log_ = &log; + + try + { + name_values opts (expand_options (options)); + + // Read module implementation configuration. + // + init (opts); + + // Read brep::module configuration. + // + static option_descriptions od ( + convert (options::module::description ())); + + name_values mo (filter (opts, od)); + name_value_scanner s (mo); + options::module o (s, cli::unknown_mode::fail, cli::unknown_mode::fail); + + verb_ = o.verbosity (); + initialized_ = true; + } + catch (const server_error& e) + { + log_write (e.data); + throw runtime_error ("initialization failed"); + } + catch (const cli::exception& e) + { + ostringstream o; + e.print (o); + throw runtime_error (o.str ()); + } + } + + void module:: + init (const name_values& options) + { + name_value_scanner s (options); + init (s); + assert (!s.more ()); // Module didn't handle its options. + } + + module:: + module (): log_writer_ (bind (&module::log_write, this, _1)) {} + + // Custom copy constructor is required to initialize log_writer_ properly. + // + module:: + module (const module& m): module () + { + verb_ = m.verb_; + initialized_ = m.initialized_; + } + +// For function func declared like this: +// using B = std::string (*)(int); +// using A = B (*)(int,int); +// A func(B (*)(char),B (*)(wchar_t)); +// __PRETTY_FUNCTION__ looks like this: +// virtual std::string (* (* brep::search::func(std::string (* (*)(char))(int) +// ,std::string (* (*)(wchar_t))(int)) const)(int, int))(int) +// + string module:: + func_name (const char* pretty_name) + { + const char* e (strchr (pretty_name, ')')); + + if (e && e > pretty_name) + { + // Position e at last matching '(' which is the beginning of the + // argument list.. + // + size_t d (1); + + do + { + switch (*--e) + { + case ')': ++d; break; + case '(': --d; break; + } + } + while (d && e > pretty_name); + + if (!d && e > pretty_name) + { + // Position e at the character following the function name. + // + while (e > pretty_name && + (*e != '(' || *(e - 1) == ' ' || *(e - 1) == ')')) + --e; + + if (e > pretty_name) + { + // Position b at the beginning of the qualified function name. + // + const char* b (e); + while (--b > pretty_name && *b != ' '); + if (*b == ' ') ++b; + + return string (b, e - b); + } + } + } + + throw invalid_argument ("::brep::module::func_name"); + } + + void module:: + log_write (const diag_data& d) const + { + if (log_ == nullptr) + return; // No backend yet. + + //@@ Cast log_ to apache::log and write the records. + // + auto al (dynamic_cast<web::apache::log*> (log_)); + + if (al) + { + // Considered using lambda for mapping but looks too verbose while can + // be a bit safer in runtime. + // + // Use APLOG_INFO (as opposed to APLOG_TRACE1) as a mapping for + // severity::trace. "LogLevel trace1" configuration directive switches + // on the avalanche of log messages from various modules. Would be good + // to avoid wading through them. + // + static int s[] = {APLOG_ERR, APLOG_WARNING, APLOG_INFO, APLOG_INFO}; + + for (const auto& e: d) + { + string name; + + try + { + name = func_name (e.name); + } + catch (const invalid_argument&) + { + // Log "pretty" function description, see in log file & fix. + name = e.name; + } + + al->write (e.loc.file.c_str (), + e.loc.line, + name.c_str (), + s[static_cast<size_t> (e.sev)], + e.msg.c_str ()); + } + } + } + + void module:: + version (log& l) + { + log_ = &l; + version (); + } + + // module::name_value_scanner + // + module::name_value_scanner:: + name_value_scanner (const name_values& nv) noexcept + : name_values_ (nv), + i_ (nv.begin ()), + name_ (true) + { + } + + bool module::name_value_scanner:: + more () + { + return i_ != name_values_.end (); + } + + const char* module::name_value_scanner:: + peek () + { + if (i_ != name_values_.end ()) + return name_ ? i_->name.c_str () : i_->value->c_str (); + else + throw cli::eos_reached (); + } + + const char* module::name_value_scanner:: + next () + { + if (i_ != name_values_.end ()) + { + const char* r (name_ ? i_->name.c_str () : i_->value->c_str ()); + skip (); + return r; + } + else + throw cli::eos_reached (); + } + + void module::name_value_scanner:: + skip () + { + if (i_ != name_values_.end ()) + { + if (name_) + { + if (i_->value) + name_ = false; + else + ++i_; + } + else + { + ++i_; + name_ = true; + } + } + else + throw cli::eos_reached (); + } +} diff --git a/mod/options-types b/mod/options-types new file mode 100644 index 0000000..e696139 --- /dev/null +++ b/mod/options-types @@ -0,0 +1,32 @@ +// file : mod/options-types -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_OPTIONS_TYPES +#define MOD_OPTIONS_TYPES + +#include <brep/types> +#include <brep/utility> + +namespace brep +{ + // brep types + // + enum class page_form + { + full, + brief + }; + + struct page_menu + { + string label; + string link; + + page_menu () = default; + page_menu (string b, string l): label (move (b)), link (move (l)) {} + }; + +} + +#endif // MOD_OPTIONS_TYPES diff --git a/mod/options.cli b/mod/options.cli new file mode 100644 index 0000000..ba8b37b --- /dev/null +++ b/mod/options.cli @@ -0,0 +1,211 @@ +// file : mod/options.cli -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +include <web/xhtml-fragment>; + +include <brep/types>; + +include <mod/options-types>; + +namespace brep +{ + // Web module configuration options. + // + namespace options + { + // Option groups. + // + class module + { + dir_path root = "/" + { + "<path>" + "Repository root. That is, this is the part of the URL between the + host name and the start of the repository. For example, root value + '\cb{/pkg}' means the repository URL is http://example.org/pkg/. + Specify '\cb{/}' to use the web server root (http://example.org/)." + } + + uint16_t verbosity = 0 + { + "<level>", + "Trace verbosity level. Level 0 disables tracing, which is also the + default." + } + }; + + class db + { + string db-user + { + "<user>", + "Database user name. If not specified, then operating system (login) + name is used." + } + + string db-password + { + "<pass>", + "Database password. If not specified, then login without password is + expected to work." + } + + string db-name = "brep" + { + "<name>", + "Database name. If not specified, then '\cb{brep}' is used by + default." + } + + string db-host + { + "<host>", + "Database host name, address, or socket. If not specified, then + connect to \cb{localhost} using the operating system-default + mechanism (Unix-domain socket, etc)." + } + + uint16_t db-port = 0 + { + "<port>", + "Database port number. If not specified, the default port is used." + } + + size_t db-max-connections = 5 + { + "<num>", + "The maximum number of concurrent database connections per web server + process. If 0, then no limitation is applied. The default is 5." + } + + size_t db-retry = 10 + { + "<num>", + "The maximum number of times to retry database transactions in the + face of recoverable failures (deadlock, loss of connection, etc). The + default is 10." + } + }; + + class page + { + web::xhtml::fragment logo + { + "<xhtml>", + "Web page logo. It is displayed in the page header aligned to the left + edge. The value is treated as an XHTML5 fragment." + } + + vector<page_menu> menu; + { + "<label=link>", + "Web page menu. Each entry is displayed in the page header in the + order specified and aligned to the right edge. A link target that + starts with '\cb{/}' or contains '\cb{:}' is used as is. Otherwise, + it is prefixed with the repository web interface root." + } + }; + + class search + { + uint16_t search-results = 10 + { + "<num>", + "Number of results per page. The default is 10." + } + + uint16_t search-pages = 5 + { + "<num>", + "Number of pages in navigation (pager). The default is 5." + } + }; + + class package + { + uint16_t package-description = 500 + { + "<len>", + "Number of package description characters to display in brief pages. + The default is 500 (~ 80 characters * 6 lines)." + } + + uint16_t package-changes = 5000; + { + "<len>", + "Number of package changes characters to display in brief pages. The + default is 5000 (~ 80 chars x 60 lines)." + } + }; + + // Module options. + // + class package_search: search, db, page, module + { + }; + + class package_details: package, search, db, page, module + { + }; + + class package_version_details: package, db, page, module + { + }; + + class repository_details: db, page, module + { + }; + + class repository_root: module + { + }; + } + + // Web module HTTP request parameters. + // + namespace params + { + // Use parameters long names in the C++ code, short aliases (if present) + // in HTTP URL. + // + class package_search + { + // Display package search result list starting from this page. + // + uint16_t page | p; + + // Package search criteria. + // + string query | q; + }; + + class package_details + { + // Display package version search result list starting from this page. + // + uint16_t page | p; + + // Package version search criteria. + // + string query | q; + + // Page form. + // + page_form form | f = page_form::brief; + }; + + class package_version_details + { + // Page form. + // + page_form form | f = page_form::brief; + }; + + class repository_details + { + // No parameters so far. + // + }; + } +} diff --git a/mod/page b/mod/page new file mode 100644 index 0000000..b326d6c --- /dev/null +++ b/mod/page @@ -0,0 +1,403 @@ +// file : mod/page -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef MOD_PAGE +#define MOD_PAGE + +#include <xml/forward> + +#include <web/xhtml-fragment> + +#include <brep/types> +#include <brep/utility> + +#include <brep/package> + +#include <mod/options-types> // page_menu + +namespace brep +{ + // Page common building blocks. + // + + // Generates CSS link elements. + // + class CSS_LINKS + { + public: + CSS_LINKS (const path& p, const dir_path& r): path_ (p), root_ (r) {} + + void + operator() (xml::serializer&) const; + + private: + const path& path_; + const dir_path& root_; + }; + + // Generates page header element. + // + class DIV_HEADER + { + public: + DIV_HEADER (const dir_path& root, + const web::xhtml::fragment& logo, + const vector<page_menu>& menu): + root_ (root), logo_ (logo), menu_ (menu) {} + + void + operator() (xml::serializer&) const; + + private: + const dir_path& root_; + const web::xhtml::fragment& logo_; + const vector<page_menu>& menu_; + }; + + // Generates package search form element. + // + class FORM_SEARCH + { + public: + FORM_SEARCH (const string& q): query_ (q) {} + + void + operator() (xml::serializer&) const; + + private: + const string& query_; + }; + + // Generates counter element. + // + // It could be redunant to distinguish between singular and plural word forms + // if it wouldn't be so cheap in English, and phrase '1 Packages' wouldn't + // look that ugly. + // + class DIV_COUNTER + { + public: + DIV_COUNTER (size_t c, const char* s, const char* p) + : count_ (c), singular_ (s), plural_ (p) {} + + void + operator() (xml::serializer&) const; + + private: + size_t count_; + const char* singular_; + const char* plural_; + }; + + // Generates package name element. + // + class TR_NAME + { + public: + TR_NAME (const string& n, const string& q, const dir_path& r) + : name_ (n), query_param_ (q), root_ (r) {} + + void + operator() (xml::serializer&) const; + + private: + const string& name_; + const string& query_param_; + const dir_path& root_; + }; + + // Generates package version element. + // + class TR_VERSION + { + public: + // Display the version as a link to the package version details page. + // + TR_VERSION (const string& p, const string& v, const dir_path& r) + : package_ (&p), version_ (v), root_ (&r) {} + + // Display the version as a regular text. + // + TR_VERSION (const string& v) + : package_ (nullptr), version_ (v), root_ (nullptr) {} + + void + operator() (xml::serializer&) const; + + private: + const string* package_; + const string& version_; + const dir_path* root_; + }; + + // Generates package summary element. + // + class TR_SUMMARY + { + public: + TR_SUMMARY (const string& s): summary_ (s) {} + + void + operator() (xml::serializer&) const; + + private: + const string& summary_; + }; + + // Generates package license alternatives element. + // + class TR_LICENSE + { + public: + TR_LICENSE (const license_alternatives& l): licenses_ (l) {} + + void + operator() (xml::serializer&) const; + + private: + const license_alternatives& licenses_; + }; + + // Generates package license alternatives elements. Differs from TR_LICENSE + // by producing multiple rows instead of a single one. + // + class TR_LICENSES + { + public: + TR_LICENSES (const license_alternatives& l): licenses_ (l) {} + + void + operator() (xml::serializer&) const; + + private: + const license_alternatives& licenses_; + }; + + // Generates package tags element. + // + class TR_TAGS + { + public: + TR_TAGS (const strings& ts, const dir_path& r): tags_ (ts), root_ (r) {} + + void + operator() (xml::serializer&) const; + + private: + const strings& tags_; + const dir_path& root_; + }; + + // Generates package dependencies element. + // + class TR_DEPENDS + { + public: + TR_DEPENDS (const dependencies& d, const dir_path& r) + : dependencies_ (d), root_ (r) {} + + void + operator() (xml::serializer&) const; + + private: + const dependencies& dependencies_; + const dir_path& root_; + }; + + // Generates package requirements element. + // + class TR_REQUIRES + { + public: + TR_REQUIRES (const requirements& r): requirements_ (r) {} + + void + operator() (xml::serializer&) const; + + private: + const requirements& requirements_; + }; + + // Generates url element. + // + class TR_URL + { + public: + TR_URL (const url& u, const char* l = "url"): url_ (u), label_ (l) {} + + void + operator() (xml::serializer&) const; + + private: + const url& url_; + const char* label_; + }; + + // Generates email element. + // + class TR_EMAIL + { + public: + TR_EMAIL (const email& e, const char* l = "email") + : email_ (e), label_ (l) {} + + void + operator() (xml::serializer&) const; + + private: + const email& email_; + const char* label_; + }; + + // Generates package version priority element. + // + class TR_PRIORITY + { + public: + TR_PRIORITY (const priority& p): priority_ (p) {} + + void + operator() (xml::serializer&) const; + + private: + const priority& priority_; + }; + + // Generates package location element. + // + class TR_LOCATION + { + public: + TR_LOCATION (const string& n, const dir_path& r) + : name_ (n), root_ (r) {} + + void + operator() (xml::serializer&) const; + + private: + const string& name_; + const dir_path& root_; + }; + + // Generates package download URL element. + // + class TR_DOWNLOAD + { + public: + TR_DOWNLOAD (const string& u): url_ (u) {} + + void + operator() (xml::serializer&) const; + + private: + const string& url_; + }; + + // Generates sha256sum element. + // + class TR_SHA256SUM + { + public: + TR_SHA256SUM (const string& s): sha256sum_ (s) {} + + void + operator() (xml::serializer&) const; + + private: + const string& sha256sum_; + }; + + // Generates comment element. + // + class SPAN_COMMENT + { + public: + SPAN_COMMENT (const string& c): comment_ (c) {} + + void + operator() (xml::serializer&) const; + + private: + const string& comment_; + }; + + // Generates package description element. + // + class P_DESCRIPTION + { + public: + // Genereate full description. + // + P_DESCRIPTION (const string& d, const string& id = "") + : description_ (d), length_ (d.size ()), url_ (nullptr), id_ (id) {} + + // Genereate brief description. + // + P_DESCRIPTION (const string& d, size_t l, const string& u) + : description_ (d), length_ (l), url_ (&u) {} + + void + operator() (xml::serializer&) const; + + private: + const string& description_; + size_t length_; + const string* url_; // Full page url. + string id_; + }; + + // Generates package description element. + // + class PRE_CHANGES + { + public: + // Genereate full changes info. + // + PRE_CHANGES (const string& c) + : changes_ (c), length_ (c.size ()), url_ (nullptr) {} + + // Genereate brief changes info. + // + PRE_CHANGES (const string& c, size_t l, const string& u) + : changes_ (c), length_ (l), url_ (&u) {} + + void + operator() (xml::serializer&) const; + + private: + const string& changes_; + size_t length_; + const string* url_; // Full page url. + }; + + // Generates paging element. + // + class DIV_PAGER + { + public: + DIV_PAGER (size_t current_page, + size_t item_count, + size_t item_per_page, + size_t page_number_count, + const string& url); + + void + operator() (xml::serializer&) const; + + private: + size_t current_page_; + size_t item_count_; + size_t item_per_page_; + size_t page_number_count_; + const string& url_; + }; + + // Convert the argument to a string representing the valid HTML 5 'id' + // attribute value. + // + string + html_id (const string&); +} + +#endif // MOD_PAGE diff --git a/mod/page.cxx b/mod/page.cxx new file mode 100644 index 0000000..7fc2e90 --- /dev/null +++ b/mod/page.cxx @@ -0,0 +1,693 @@ +// file : mod/page.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/page> + +#include <set> +#include <ios> // hex, uppercase, right +#include <sstream> +#include <iomanip> // setw(), setfill() +#include <algorithm> // min() + +#include <xml/serializer> + +#include <web/xhtml> +#include <web/mime-url-encoding> + +#include <brep/package> +#include <brep/package-odb> + +using namespace std; +using namespace xml; +using namespace web; +using namespace web::xhtml; + +namespace brep +{ + // CSS_LINKS + // + void CSS_LINKS:: + operator() (serializer& s) const + { + static const path css ("@"); + + s << *LINK(REL="stylesheet", TYPE="text/css", HREF=root_ / css / path_); + } + + // DIV_HEADER + // + void DIV_HEADER:: + operator() (serializer& s) const + { + if (!logo_.empty () || !menu_.empty ()) + { + s << DIV(ID="header-bar") + << DIV(ID="header"); + + if (!logo_.empty ()) + s << DIV(ID="header-logo") << logo_ << ~DIV; + + if (!menu_.empty ()) + { + s << DIV(ID="header-menu") + << DIV(ID="header-menu-body"); + + for (const auto& m: menu_) + { + const string& l (m.link[0] == '/' || m.link.find (':') != string::npos + ? m.link + : root_.string () + m.link); + + s << A(HREF=l) << m.label << ~A; + } + + s << ~DIV + << ~DIV; + } + + s << ~DIV + << ~DIV; + } + } + + // FORM_SEARCH + // + void FORM_SEARCH:: + operator() (serializer& s) const + { + // The 'action' attribute is optional in HTML5. While the standard doesn't + // specify browser behavior explicitly for the case the attribute is + // ommited, the only reasonable behavior is to default it to the current + // document URL. + // + s << FORM(ID="search") + << TABLE(CLASS="form-table") + << TBODY + << TR + << TD(ID="search-txt") + << *INPUT(TYPE="search", NAME="q", VALUE=query_, + AUTOFOCUS="autofocus") + << ~TD + << TD(ID="search-btn") + << *INPUT(TYPE="submit", VALUE="Search") + << ~TD + << ~TR + << ~TBODY + << ~TABLE + << ~FORM; + } + + // DIV_COUNTER + // + void DIV_COUNTER:: + operator() (serializer& s) const + { + s << DIV(ID="count") + << count_ << " " + << (count_ % 10 == 1 && count_ % 100 != 11 ? singular_ : plural_) + << ~DIV; + } + + // TR_NAME + // + void TR_NAME:: + operator() (serializer& s) const + { + s << TR(CLASS="name") + << TH << "name" << ~TH + << TD + << SPAN(CLASS="value") + << A + << HREF + + // Propagate search criteria to the package details page. + // + << root_ / path (mime_url_encode (name_)) << query_param_ + + << ~HREF + << name_ + << ~A + << ~SPAN + << ~TD + << ~TR; + } + + void TR_VERSION:: + operator() (serializer& s) const + { + s << TR(CLASS="version") + << TH << "version" << ~TH + << TD + << SPAN(CLASS="value"); + + if (package_ == nullptr) + s << version_; + else + { + assert (root_ != nullptr); + s << A(HREF=*root_ / path (mime_url_encode (*package_)) / path (version_)) + << version_ + << ~A; + } + + s << ~SPAN + << ~TD + << ~TR; + } + + // TR_SUMMARY + // + void TR_SUMMARY:: + operator() (serializer& s) const + { + s << TR(CLASS="summary") + << TH << "summary" << ~TH + << TD << SPAN(CLASS="value") << summary_ << ~SPAN << ~TD + << ~TR; + } + + // TR_LICENSE + // + void TR_LICENSE:: + operator() (serializer& s) const + { + s << TR(CLASS="license") + << TH << "license" << ~TH + << TD + << SPAN(CLASS="value"); + + for (const auto& la: licenses_) + { + if (&la != &licenses_[0]) + s << " " << EM << "or" << ~EM << " "; + + bool m (la.size () > 1); + + if (m) + s << "("; + + for (const auto& l: la) + { + if (&l != &la[0]) + s << " " << EM << "and" << ~EM << " "; + + s << l; + } + + if (m) + s << ")"; + } + + s << ~SPAN + << ~TD + << ~TR; + } + + // TR_LICENSES + // + void TR_LICENSES:: + operator() (serializer& s) const + { + for (const auto& la: licenses_) + { + s << TR(CLASS="license") + << TH << "license" << ~TH + << TD + << SPAN(CLASS="value"); + + for (const auto& l: la) + { + if (&l != &la[0]) + s << " " << EM << "and" << ~EM << " "; + + s << l; + } + + s << ~SPAN + << SPAN_COMMENT (la.comment) + << ~TD + << ~TR; + } + } + + // TR_TAGS + // + void TR_TAGS:: + operator() (serializer& s) const + { + if (!tags_.empty ()) + { + s << TR(CLASS="tags") + << TH << "tags" << ~TH + << TD + << SPAN(CLASS="value"); + + for (const auto& t: tags_) + { + if (&t != &tags_[0]) + s << " "; + + s << A << HREF << root_ << "?q=" << mime_url_encode (t) << ~HREF + << t + << ~A; + } + + s << ~SPAN + << ~TD + << ~TR; + } + } + + // TR_DEPENDS + // + void TR_DEPENDS:: + operator() (serializer& s) const + { + s << TR(CLASS="depends") + << TH << "depends" << ~TH + << TD + << SPAN(CLASS="value") + << dependencies_.size (); + + if (!dependencies_.empty ()) + s << "; "; + + for (const auto& d: dependencies_) + { + if (&d != &dependencies_[0]) + s << ", "; + + if (d.conditional) + s << "?"; + + // Suppress package name duplicates. + // + set<string> names; + for (const auto& da: d) + names.emplace (da.name ()); + + bool mult (names.size () > 1); + + if (mult) + s << "("; + + bool first (true); + for (const auto& da: d) + { + string n (da.name ()); + if (names.find (n) != names.end ()) + { + names.erase (n); + + if (first) + first = false; + else + s << " | "; + + shared_ptr<package> p (da.package.load ()); + assert (p->internal () || !p->other_repositories.empty ()); + + shared_ptr<repository> r ( + p->internal () + ? p->internal_repository.load () + : p->other_repositories[0].load ()); + + auto en (mime_url_encode (n)); + + if (r->url) + s << A(HREF=*r->url + en) << n << ~A; + else if (p->internal ()) + s << A(HREF=root_ / path (en)) << n << ~A; + else + // Display the dependency as a plain text if no repository URL + // available. + // + s << n; + } + } + + if (mult) + s << ")"; + } + + s << ~SPAN + << ~TD + << ~TR; + } + + // TR_REQUIRES + // + void TR_REQUIRES:: + operator() (serializer& s) const + { + // If there are no requirements, then we omit it, unlike depends, where we + // show 0 explicitly. + // + if (requirements_.empty ()) + return; + + s << TR(CLASS="requires") + << TH << "requires" << ~TH + << TD + << SPAN(CLASS="value") + << requirements_.size () << "; "; + + for (const auto& r: requirements_) + { + if (&r != &requirements_[0]) + s << ", "; + + if (r.conditional) + s << "?"; + + if (r.empty ()) + { + // If there is no requirement alternatives specified, then + // print the comment first word. + // + const auto& c (r.comment); + if (!c.empty ()) + { + auto n (c.find (' ')); + s << string (c, 0, n); + + if (n != string::npos) + s << "..."; + } + } + else + { + bool mult (r.size () > 1); + + if (mult) + s << "("; + + for (const auto& ra: r) + { + if (&ra != &r[0]) + s << " | "; + + s << ra; + } + + if (mult) + s << ")"; + } + } + + s << ~SPAN + << ~TD + << ~TR; + } + + // TR_URL + // + void TR_URL:: + operator() (serializer& s) const + { + s << TR(CLASS=label_) + << TH << label_ << ~TH + << TD + << SPAN(CLASS="value") << A(HREF=url_) << url_ << ~A << ~SPAN + << SPAN_COMMENT (url_.comment) + << ~TD + << ~TR; + } + + // TR_EMAIL + // + void TR_EMAIL:: + operator() (serializer& s) const + { + s << TR(CLASS=label_) + << TH << label_ << ~TH + << TD + << SPAN(CLASS="value") + << A(HREF="mailto:" + email_) << email_ << ~A + << ~SPAN + << SPAN_COMMENT (email_.comment) + << ~TD + << ~TR; + } + + // TR_PRIORITY + // + void TR_PRIORITY:: + operator() (serializer& s) const + { + static const strings priority_names ({"low", "medium", "high", "security"}); + assert (priority_ < priority_names.size ()); + + s << TR(CLASS="priority") + << TH << "priority" << ~TH + << TD + << SPAN(CLASS="value") << priority_names[priority_] << ~SPAN + << SPAN_COMMENT (priority_.comment) + << ~TD + << ~TR; + } + + // TR_LOCATION + // + void TR_LOCATION:: + operator() (serializer& s) const + { + s << TR(CLASS="location") + << TH << "location" << ~TH + << TD + << SPAN(CLASS="value") + << A + << HREF + << root_ << "?about#" << mime_url_encode (html_id (name_)) + << ~HREF + << name_ + << ~A + << ~SPAN + << ~TD + << ~TR; + } + + // TR_DOWNLOAD + // + void TR_DOWNLOAD:: + operator() (serializer& s) const + { + s << TR(CLASS="download") + << TH << "download" << ~TH + << TD + << SPAN(CLASS="value") << A(HREF=url_) << url_ << ~A << ~SPAN + << ~TD + << ~TR; + } + + // TR_SHA256SUM + // + void TR_SHA256SUM:: + operator() (serializer& s) const + { + s << TR(CLASS="sha256") + << TH << "sha256" << ~TH + << TD << SPAN(CLASS="value") << sha256sum_ << ~SPAN << ~TD + << ~TR; + } + + // SPAN_COMMENT + // + void SPAN_COMMENT:: + operator() (serializer& s) const + { + if (size_t l = comment_.size ()) + s << SPAN(CLASS="comment") + << (comment_.back () == '.' ? string (comment_, 0, l - 1) : comment_) + << ~SPAN; + } + + // P_DESCRIPTION + // + void P_DESCRIPTION:: + operator() (serializer& s) const + { + if (description_.empty ()) + return; + + auto n (description_.find_first_of (" \t\n", length_)); + bool full (n == string::npos); // Description length is below the limit. + + // Truncate description if length exceed the limit. + // + const string& d (full ? description_ : string (description_, 0, n)); + + // Format the description into paragraphs, recognizing a blank line as + // paragraph separator, and replacing single newlines with a space. + // + s << P; + + if (!id_.empty ()) + s << ID(id_); + + bool nl (false); // The previous character is '\n'. + for (const auto& c: d) + { + if (c == '\n') + { + if (nl) + { + s << ~P << P; + nl = false; + } + else + nl = true; // Delay printing until the next character. + } + else + { + if (nl) + { + s << ' '; // Replace the previous newline with a space. + nl = false; + } + + s << c; + } + } + + if (!full) + { + assert (url_ != nullptr); + s << "... " << A(HREF=*url_) << "More" << ~A; + } + + s << ~P; + } + + // PRE_CHANGES + // + void PRE_CHANGES:: + operator() (serializer& s) const + { + if (changes_.empty ()) + return; + + auto n (changes_.find_first_of (" \t\n", length_)); + bool full (n == string::npos); // Changes length is below the limit. + + // Truncate changes if length exceed the limit. + // + const string& c (full ? changes_ : string (changes_, 0, n)); + s << PRE(ID="changes") << c; + + if (!full) + { + assert (url_ != nullptr); + s << "... " << A(HREF=*url_) << "More" << ~A; + } + + s << ~PRE; + } + + // DIV_PAGER + // + DIV_PAGER:: + DIV_PAGER (size_t current_page, + size_t item_count, + size_t item_per_page, + size_t page_number_count, + const string& url) + : current_page_ (current_page), + item_count_ (item_count), + item_per_page_ (item_per_page), + page_number_count_ (page_number_count), + url_ (url) + { + } + + void DIV_PAGER:: + operator() (serializer& s) const + { + if (item_count_ == 0 || item_per_page_ == 0) + return; + + size_t pcount (item_count_ / item_per_page_); // Page count. + + if (item_count_ % item_per_page_) + ++pcount; + + if (pcount > 1) + { + auto url ( + [this](size_t page) -> string + { + return page == 0 + ? url_ + : url_ + (url_.find ('?') == string::npos ? "?p=" : "&p=") + + to_string (page); + }); + + s << DIV(ID="pager"); + + if (current_page_ > 0) + s << A(ID="prev", HREF=url (current_page_ - 1)) << "Prev" << ~A; + + if (page_number_count_) + { + size_t offset (page_number_count_ / 2); + size_t from (current_page_ > offset ? current_page_ - offset : 0); + size_t to (min (from + page_number_count_, pcount)); + + for (size_t p (from); p < to; ++p) + { + s << A(HREF=url (p)); + + if (p == current_page_) + s << ID("curr"); + + s << p + 1 + << ~A; + } + } + + if (current_page_ < pcount - 1) + s << A(ID="next", HREF=url (current_page_ + 1)) << "Next" << ~A; + + s << ~DIV; + } + } + + // Convert the argument to a string conformant to the section + // "3.2.5.1 The id attribute" of the HTML 5 specification at + // http://www.w3.org/TR/html5/dom.html#the-id-attribute. + // + string + html_id (const string& v) + { + ostringstream o; + o << hex << uppercase << right << setfill ('0'); + + // Replace space characters (as specified at + // http://www.w3.org/TR/html5/infrastructure.html#space-character) with + // the respective escape sequences. + // + for (auto c: v) + { + switch (c) + { + case ' ': + case '\t': + case '\n': + case '\r': + case '\f': + case '~': + { + // We use '~' as an escape character because it doesn't require + // escaping in URLs. + // + o << "~" << setw (2) << static_cast<unsigned short> (c); + break; + } + default: o << c; break; + } + } + + return o.str (); + } +} diff --git a/mod/services.cxx b/mod/services.cxx new file mode 100644 index 0000000..b0c5834 --- /dev/null +++ b/mod/services.cxx @@ -0,0 +1,15 @@ +// file : mod/services.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <ap_config.h> // AP_MODULE_DECLARE_DATA + +#include <web/apache/service> + +#include <brep/types> +#include <brep/utility> + +#include <mod/mod-repository-root> + +static brep::repository_root mod; +web::apache::service AP_MODULE_DECLARE_DATA brep_module ("brep", mod); diff --git a/mod/types-parsers b/mod/types-parsers new file mode 100644 index 0000000..cbea1bd --- /dev/null +++ b/mod/types-parsers @@ -0,0 +1,57 @@ +// file : mod/types-parsers -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +// CLI parsers, included into the generated source files. +// + +#ifndef MOD_TYPES_PARSERS +#define MOD_TYPES_PARSERS + +#include <web/xhtml-fragment> + +#include <brep/types> +#include <brep/utility> + +#include <mod/options-types> + +namespace brep +{ + namespace cli + { + class scanner; + + template <typename T> + struct parser; + + template <> + struct parser<dir_path> + { + static void + parse (dir_path&, scanner&); + }; + + template <> + struct parser<page_form> + { + static void + parse (page_form&, scanner&); + }; + + template <> + struct parser<page_menu> + { + static void + parse (page_menu&, scanner&); + }; + + template <> + struct parser<web::xhtml::fragment> + { + static void + parse (web::xhtml::fragment&, scanner&); + }; + } +} + +#endif // MOD_TYPES_PARSERS diff --git a/mod/types-parsers.cxx b/mod/types-parsers.cxx new file mode 100644 index 0000000..279ab58 --- /dev/null +++ b/mod/types-parsers.cxx @@ -0,0 +1,114 @@ +// file : mod/types-parsers.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include <mod/types-parsers> + +#include <mod/options> + +using namespace std; +using namespace web::xhtml; + +namespace brep +{ + namespace cli + { + // Parse path. + // + template <typename T> + static void + parse_path (T& x, scanner& s) + { + const char* o (s.next ()); + + if (!s.more ()) + throw missing_value (o); + + const char* v (s.next ()); + + try + { + x = T (v); + } + catch (const invalid_path&) + { + throw invalid_value (o, v); + } + } + + void parser<dir_path>:: + parse (dir_path& x, scanner& s) + { + parse_path (x, s); + } + + // Parse page_form. + // + void parser<page_form>:: + parse (page_form& x, scanner& s) + { + const char* o (s.next ()); + + if (!s.more ()) + throw missing_value (o); + + const string v (s.next ()); + if (v == "full") + x = page_form::full; + else if (v == "brief") + x = page_form::brief; + else + throw invalid_value (o, v); + } + + // Parse page_menu. + // + void parser<page_menu>:: + parse (page_menu& x, scanner& s) + { + const char* o (s.next ()); + + if (!s.more ()) + throw missing_value (o); + + const string v (s.next ()); + + auto p (v.find ('=')); + if (p != string::npos) + { + string label (v, 0, p); + string link (v, p + 1); + + if (!label.empty ()) + { + x = page_menu (move (label), move (link)); + return; + } + } + + throw invalid_value (o, v); + } + + // Parse web::xhtml::fragment. + // + void parser<fragment>:: + parse (fragment& x, scanner& s) + { + const char* o (s.next ()); + + if (!s.more ()) + throw missing_value (o); + + const char* v (s.next ()); + + try + { + x = fragment (v, o); + } + catch (const xml::parsing&) + { + throw invalid_value (o, v); + } + } + } +} |