Răsfoiți Sursa

Merge branch 'BACK_TO_PHP'

Iñigo Valentin 2 ani în urmă
părinte
comite
5b8c60b87e
100 a modificat fișierele cu 3551 adăugiri și 1587 ștergeri
  1. 0 10
      .gitattributes
  2. 28 39
      .gitignore
  3. 0 1
      .ruby-version
  4. 0 60
      Gemfile
  5. 0 235
      Gemfile.lock
  6. 0 9
      README.es.md
  7. 0 9
      README.eu.md
  8. 0 9
      README.md
  9. 0 6
      Rakefile
  10. 0 3
      app/assets/config/manifest.js
  11. 0 16
      app/assets/javascripts/help.js
  12. 0 103
      app/assets/javascripts/main.js
  13. 0 48
      app/assets/javascripts/project.js
  14. 0 14
      app/assets/stylesheets/application.css
  15. 0 51
      app/assets/stylesheets/help.css
  16. 0 3
      app/assets/stylesheets/profiles.scss
  17. 0 4
      app/channels/application_cable/channel.rb
  18. 0 4
      app/channels/application_cable/connection.rb
  19. 0 32
      app/controllers/application_controller.rb
  20. 0 0
      app/controllers/concerns/.keep
  21. 0 53
      app/controllers/contact_controller.rb
  22. 0 5
      app/controllers/error_controller.rb
  23. 0 4
      app/controllers/help_controller.rb
  24. 0 5
      app/controllers/main_controller.rb
  25. 0 6
      app/controllers/profile_controller.rb
  26. 0 16
      app/controllers/projects_controller.rb
  27. 0 2
      app/helpers/application_helper.rb
  28. 0 2
      app/helpers/error_helper.rb
  29. 0 2
      app/helpers/help_helper.rb
  30. 0 2
      app/helpers/profile_helper.rb
  31. 0 2
      app/helpers/projects_helper.rb
  32. 0 6
      app/javascript/channels/consumer.js
  33. 0 5
      app/javascript/channels/index.js
  34. 0 13
      app/javascript/packs/application.js
  35. 0 7
      app/jobs/application_job.rb
  36. 0 11
      app/mailers/application_mailer.rb
  37. 0 3
      app/models/application_record.rb
  38. 0 0
      app/models/concerns/.keep
  39. 0 4
      app/models/license.rb
  40. 0 3
      app/models/mail_adress.rb
  41. 0 5
      app/models/message.rb
  42. 0 19
      app/models/project.rb
  43. 0 4
      app/models/project_url.rb
  44. 0 4
      app/models/resume.rb
  45. 0 6
      app/models/social_link.rb
  46. 0 5
      app/models/technology.rb
  47. 0 3
      app/models/text.rb
  48. 0 9
      app/models/url_target.rb
  49. 0 13
      app/models/user.rb
  50. 0 7
      app/models/user_social_link.rb
  51. 0 26
      app/views/application_mailer/message_email.html.erb
  52. 0 18
      app/views/contact/error.erb
  53. 0 5
      app/views/contact/success.erb
  54. 0 31
      app/views/error/error_404.html.erb
  55. 0 41
      app/views/help/index.html.erb
  56. 0 90
      app/views/layouts/_contact_form.html.erb
  57. 0 72
      app/views/layouts/_footer.html.erb
  58. 0 34
      app/views/layouts/_header.html.erb
  59. 0 22
      app/views/layouts/application.html.erb
  60. 0 18
      app/views/layouts/mailer.html.erb
  61. 0 1
      app/views/layouts/mailer.text.erb
  62. 0 85
      app/views/main/index.html.erb
  63. 0 64
      app/views/profile/index.html.erb
  64. 0 43
      app/views/projects/index.html.erb
  65. 0 155
      app/views/projects/show.html.erb
  66. 26 0
      application/Application.php
  67. 107 0
      application/Context.php
  68. 207 0
      application/Controller.php
  69. 22 0
      application/config/config.ini
  70. 84 0
      application/entity/Cv.php
  71. 18 0
      application/entity/Entity.php
  72. 77 0
      application/entity/Lang.php
  73. 113 0
      application/entity/License.php
  74. 416 0
      application/entity/Project.php
  75. 109 0
      application/entity/Project_Image.php
  76. 37 0
      application/entity/Project_Tag.php
  77. 86 0
      application/entity/Project_Type.php
  78. 97 0
      application/entity/Project_Url.php
  79. 96 0
      application/entity/Project_Url_Type.php
  80. 85 0
      application/entity/Social.php
  81. 231 0
      application/entity/User.php
  82. 126 0
      application/helper/DB_Helper.php
  83. 96 0
      application/helper/HTML_Helper.php
  84. 20 0
      application/helper/Helper.php
  85. 59 0
      application/helper/NET_Helper.php
  86. 266 0
      application/helper/TEXT_Helper.php
  87. 44 0
      application/helper/db.php
  88. 45 0
      application/page/Error_Page.php
  89. 82 0
      application/page/Help_Page.php
  90. 52 0
      application/page/Home_Page.php
  91. 320 0
      application/page/Page.php
  92. 75 0
      application/page/Profile_Page.php
  93. 47 0
      application/page/Project_Page.php
  94. 45 0
      application/page/Projects_Page.php
  95. 32 0
      application/page/Robots_Page.php
  96. 74 0
      application/page/Sitemap_Page.php
  97. 122 0
      application/util/Log.php
  98. 90 0
      application/util/Path.php
  99. 72 0
      application/util/URL.php
  100. 45 0
      application/view/error.php

+ 0 - 10
.gitattributes

@@ -1,10 +0,0 @@
-# See https://git-scm.com/docs/gitattributes for more about git attribute files.
-
-# Mark the database schema as having been generated.
-db/schema.rb linguist-generated
-
-# Mark the yarn lockfile as having been generated.
-yarn.lock linguist-generated
-
-# Mark any vendored files as having been vendored.
-vendor/* linguist-vendored

+ 28 - 39
.gitignore

@@ -1,44 +1,33 @@
-# See https://help.github.com/articles/ignoring-files for more about ignoring files.
-#
-# If you find yourself ignoring temporary files generated by your text editor
-# or operating system, you probably want to add a global ignore instead:
-#   git config --global core.excludesfile '~/.gitignore_global'
+#OS files
+.directory
+.DS_Store
+.DS_Store?
+._*
+.Trashes
+ehthumbs.db
+Thumbs.db
+*.swp
 
-# Ignore bundler config.
-/.bundle
-
-# Ignore all logfiles and tempfiles.
-/log/*
-/tmp/*
-!/log/.keep
-!/tmp/.keep
-
-# Ignore pidfiles, but keep the directory.
-/tmp/pids/*
-!/tmp/pids/
-!/tmp/pids/.keep
+#IDE files
+.kateproject.d/
+.kateproject
+.project
+.settings
+.externalToolBuilders
+*.kate-swp
 
-# Ignore uploaded files in development.
-/storage/*
-!/storage/.keep
-/custom_data
-/public/assets
-.byebug_history
+#Local server and deploy tools
+deploy
+server_*
+.php_pid
 
-# Ignore master key for decrypting credentials and more.
-/config/master.key
+#SQL custom data
+sql/XX_*
 
-/public/packs
-/public/packs-test
-/node_modules
-/yarn-error.log
-yarn-debug.log*
-.yarn-integrity
-.project
-.custom-data
+#Passwords file
+.htpasswd*
+application/config/config.ini_*
 
-# Credentials and keys
-/config/credentials/*.key
-/config/credentials/*.yml.enc
-/config/credentials.yml.enc
-/config/master.key
+#Content images
+public/img/content/
+application/resources/

+ 0 - 1
.ruby-version

@@ -1 +0,0 @@
-ruby-2.7.4

+ 0 - 60
Gemfile

@@ -1,60 +0,0 @@
-source 'https://rubygems.org'
-git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-
-ruby '2.7.4'
-
-# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
-gem 'rails', '~> 6.1.4', '>= 6.1.4.1'
-# Use mysql as the database for Active Record
-gem 'mysql2', '~> 0.5'
-# Use Puma as the app server
-gem 'puma', '~> 5.0'
-# Use SCSS for stylesheets
-gem 'sass-rails', '>= 6'
-# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
-gem 'webpacker', '~> 5.0'
-# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
-gem 'turbolinks', '~> 5'
-# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
-gem 'jbuilder', '~> 2.7'
-# Use Redis adapter to run Action Cable in production
-# gem 'redis', '~> 4.0'
-# Use Active Model has_secure_password
-# gem 'bcrypt', '~> 3.1.7'
-
-# Use Active Storage variant
-# gem 'image_processing', '~> 1.2'
-
-# Reduces boot times through caching; required in config/boot.rb
-gem 'bootsnap', '>= 1.4.4', require: false
-
-gem 'turbolinks_render'
-
-gem 'loofah', '>= 2.12.0'
-
-group :development, :test do
-  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
-  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
-end
-
-group :development do
-  # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
-  gem 'web-console', '>= 4.1.0'
-  # Display performance information such as SQL time and flame graphs for each request in your browser.
-  # Can be configured to work on production as well see: https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md
-  #gem 'rack-mini-profiler', '~> 2.0'
-  gem 'listen', '~> 3.3'
-  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
-  gem 'spring'
-end
-
-group :test do
-  # Adds support for Capybara system testing and selenium driver
-  gem 'capybara', '>= 3.26'
-  gem 'selenium-webdriver'
-  # Easy installation and use of web drivers to run system tests with browsers
-  gem 'webdrivers'
-end
-
-# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

+ 0 - 235
Gemfile.lock

@@ -1,235 +0,0 @@
-GEM
-  remote: https://rubygems.org/
-  specs:
-    actioncable (6.1.4.1)
-      actionpack (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      nio4r (~> 2.0)
-      websocket-driver (>= 0.6.1)
-    actionmailbox (6.1.4.1)
-      actionpack (= 6.1.4.1)
-      activejob (= 6.1.4.1)
-      activerecord (= 6.1.4.1)
-      activestorage (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      mail (>= 2.7.1)
-    actionmailer (6.1.4.1)
-      actionpack (= 6.1.4.1)
-      actionview (= 6.1.4.1)
-      activejob (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      mail (~> 2.5, >= 2.5.4)
-      rails-dom-testing (~> 2.0)
-    actionpack (6.1.4.1)
-      actionview (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      rack (~> 2.0, >= 2.0.9)
-      rack-test (>= 0.6.3)
-      rails-dom-testing (~> 2.0)
-      rails-html-sanitizer (~> 1.0, >= 1.2.0)
-    actiontext (6.1.4.1)
-      actionpack (= 6.1.4.1)
-      activerecord (= 6.1.4.1)
-      activestorage (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      nokogiri (>= 1.8.5)
-    actionview (6.1.4.1)
-      activesupport (= 6.1.4.1)
-      builder (~> 3.1)
-      erubi (~> 1.4)
-      rails-dom-testing (~> 2.0)
-      rails-html-sanitizer (~> 1.1, >= 1.2.0)
-    activejob (6.1.4.1)
-      activesupport (= 6.1.4.1)
-      globalid (>= 0.3.6)
-    activemodel (6.1.4.1)
-      activesupport (= 6.1.4.1)
-    activerecord (6.1.4.1)
-      activemodel (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-    activestorage (6.1.4.1)
-      actionpack (= 6.1.4.1)
-      activejob (= 6.1.4.1)
-      activerecord (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      marcel (~> 1.0.0)
-      mini_mime (>= 1.1.0)
-    activesupport (6.1.4.1)
-      concurrent-ruby (~> 1.0, >= 1.0.2)
-      i18n (>= 1.6, < 2)
-      minitest (>= 5.1)
-      tzinfo (~> 2.0)
-      zeitwerk (~> 2.3)
-    addressable (2.8.0)
-      public_suffix (>= 2.0.2, < 5.0)
-    bindex (0.8.1)
-    bootsnap (1.9.3)
-      msgpack (~> 1.0)
-    builder (3.2.4)
-    byebug (11.1.3)
-    capybara (3.36.0)
-      addressable
-      matrix
-      mini_mime (>= 0.1.3)
-      nokogiri (~> 1.8)
-      rack (>= 1.6.0)
-      rack-test (>= 0.6.3)
-      regexp_parser (>= 1.5, < 3.0)
-      xpath (~> 3.2)
-    childprocess (4.1.0)
-    concurrent-ruby (1.1.9)
-    crass (1.0.6)
-    erubi (1.10.0)
-    ffi (1.15.4)
-    globalid (1.0.0)
-      activesupport (>= 5.0)
-    i18n (1.8.11)
-      concurrent-ruby (~> 1.0)
-    jbuilder (2.11.3)
-      activesupport (>= 5.0.0)
-    listen (3.7.0)
-      rb-fsevent (~> 0.10, >= 0.10.3)
-      rb-inotify (~> 0.9, >= 0.9.10)
-    loofah (2.12.0)
-      crass (~> 1.0.2)
-      nokogiri (>= 1.5.9)
-    mail (2.7.1)
-      mini_mime (>= 0.1.1)
-    marcel (1.0.2)
-    matrix (0.4.2)
-    method_source (1.0.0)
-    mini_mime (1.1.2)
-    mini_portile2 (2.6.1)
-    minitest (5.14.4)
-    msgpack (1.4.2)
-    mysql2 (0.5.3)
-    nio4r (2.5.8)
-    nokogiri (1.12.5)
-      mini_portile2 (~> 2.6.1)
-      racc (~> 1.4)
-    public_suffix (4.0.6)
-    puma (5.5.2)
-      nio4r (~> 2.0)
-    racc (1.6.0)
-    rack (2.2.3)
-    rack-proxy (0.7.0)
-      rack
-    rack-test (1.1.0)
-      rack (>= 1.0, < 3)
-    rails (6.1.4.1)
-      actioncable (= 6.1.4.1)
-      actionmailbox (= 6.1.4.1)
-      actionmailer (= 6.1.4.1)
-      actionpack (= 6.1.4.1)
-      actiontext (= 6.1.4.1)
-      actionview (= 6.1.4.1)
-      activejob (= 6.1.4.1)
-      activemodel (= 6.1.4.1)
-      activerecord (= 6.1.4.1)
-      activestorage (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      bundler (>= 1.15.0)
-      railties (= 6.1.4.1)
-      sprockets-rails (>= 2.0.0)
-    rails-dom-testing (2.0.3)
-      activesupport (>= 4.2.0)
-      nokogiri (>= 1.6)
-    rails-html-sanitizer (1.4.2)
-      loofah (~> 2.3)
-    railties (6.1.4.1)
-      actionpack (= 6.1.4.1)
-      activesupport (= 6.1.4.1)
-      method_source
-      rake (>= 0.13)
-      thor (~> 1.0)
-    rake (13.0.6)
-    rb-fsevent (0.11.0)
-    rb-inotify (0.10.1)
-      ffi (~> 1.0)
-    regexp_parser (2.2.0)
-    rexml (3.2.5)
-    rubyzip (2.3.2)
-    sass-rails (6.0.0)
-      sassc-rails (~> 2.1, >= 2.1.1)
-    sassc (2.4.0)
-      ffi (~> 1.9)
-    sassc-rails (2.1.2)
-      railties (>= 4.0.0)
-      sassc (>= 2.0)
-      sprockets (> 3.0)
-      sprockets-rails
-      tilt
-    selenium-webdriver (4.1.0)
-      childprocess (>= 0.5, < 5.0)
-      rexml (~> 3.2, >= 3.2.5)
-      rubyzip (>= 1.2.2)
-    semantic_range (3.0.0)
-    spring (3.1.1)
-    sprockets (4.0.2)
-      concurrent-ruby (~> 1.0)
-      rack (> 1, < 3)
-    sprockets-rails (3.4.1)
-      actionpack (>= 5.2)
-      activesupport (>= 5.2)
-      sprockets (>= 3.0.0)
-    thor (1.1.0)
-    tilt (2.0.10)
-    turbolinks (5.2.1)
-      turbolinks-source (~> 5.2)
-    turbolinks-source (5.2.0)
-    turbolinks_render (0.9.21)
-      actionpack (>= 5.2.0)
-      activesupport (>= 5.2.0)
-      railties (>= 5.2.0)
-      turbolinks-source (~> 5.1)
-    tzinfo (2.0.4)
-      concurrent-ruby (~> 1.0)
-    web-console (4.2.0)
-      actionview (>= 6.0.0)
-      activemodel (>= 6.0.0)
-      bindex (>= 0.4.0)
-      railties (>= 6.0.0)
-    webdrivers (5.0.0)
-      nokogiri (~> 1.6)
-      rubyzip (>= 1.3.0)
-      selenium-webdriver (~> 4.0)
-    webpacker (5.4.3)
-      activesupport (>= 5.2)
-      rack-proxy (>= 0.6.1)
-      railties (>= 5.2)
-      semantic_range (>= 2.3.0)
-    websocket-driver (0.7.5)
-      websocket-extensions (>= 0.1.0)
-    websocket-extensions (0.1.5)
-    xpath (3.2.0)
-      nokogiri (~> 1.8)
-    zeitwerk (2.5.1)
-
-PLATFORMS
-  ruby
-
-DEPENDENCIES
-  bootsnap (>= 1.4.4)
-  byebug
-  capybara (>= 3.26)
-  jbuilder (~> 2.7)
-  listen (~> 3.3)
-  loofah (>= 2.12.0)
-  mysql2 (~> 0.5)
-  puma (~> 5.0)
-  rails (~> 6.1.4, >= 6.1.4.1)
-  sass-rails (>= 6)
-  selenium-webdriver
-  spring
-  turbolinks (~> 5)
-  turbolinks_render
-  tzinfo-data
-  web-console (>= 4.1.0)
-  webdrivers
-  webpacker (~> 5.0)
-
-RUBY VERSION
-   ruby 2.7.4p191
-
-BUNDLED WITH
-   2.1.4

+ 0 - 9
README.es.md

@@ -1,9 +0,0 @@
-## Código fuente de mi sitio personal
-### [https://inigovalentin.com](https://inigovalentin.com)
-
-Ver en otros idiomas:
--[English](README.md)
--[Español](README.es.md)
--[Euskaraz](README.eu.md)
-
-It'a basic website, a showcase for a software developer works and résumé, built on Ruby on Rails.

+ 0 - 9
README.eu.md

@@ -1,9 +0,0 @@
-## Nire gune pertsonalaren iturburu kodea 
-### [https://inigovalentin.com](https://inigovalentin.com)
-
-Beste hizkuntzak:
--[English](README.md)
--[Español](README.es.md)
--[Euskaraz](README.eu.md)
-
-Oinarrizko webgunea da, software garatzaile baten lan eta curriculumaren erakusleihoa, Ruby on Rails-en gainean eraikia.

+ 0 - 9
README.md

@@ -1,9 +0,0 @@
-## Source code of my personal site
-### [https://inigovalentin.com](https://inigovalentin.com)
-
-Other languages:
--[English](README.md)
--[Español](README.es.md)
--[Euskaraz](README.eu.md)
-
-It'a basic website, a showcase for a software developer works and résumé, built on Ruby on Rails.

+ 0 - 6
Rakefile

@@ -1,6 +0,0 @@
-# Add your own tasks in files placed in lib/tasks ending in .rake,
-# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-
-require_relative "config/application"
-
-Rails.application.load_tasks

+ 0 - 3
app/assets/config/manifest.js

@@ -1,3 +0,0 @@
-//= link_tree ../images
-//= link_directory ../stylesheets .css
-//= link_directory ../javascripts .js

+ 0 - 16
app/assets/javascripts/help.js

@@ -1,16 +0,0 @@
-function toggleHelp(id){
-    var helps = document.getElementsByClassName("help");
-    for (var i = 0; i < helps.length; i++) {
-        if (helps[i].id == id){
-            if (helps[i].classList.contains("selected")){
-                helps[i].classList.remove("selected");
-            }
-            else{
-                helps[i].classList.add("selected");
-            }
-        }
-        else{
-            helps[i].classList.remove("selected");
-        }
-    }
-}

+ 0 - 103
app/assets/javascripts/main.js

@@ -1,103 +0,0 @@
-function showContactForm(){
-    clearContactErrors();
-    document.getElementById('contact_menu').style.display = "block";
-    document.getElementById('contact_progress').style.display = "none";
-    document.getElementById('contact_confirmation').style.display = "none";
-    document.getElementById('contact_bg').style.opacity = "0.6";
-    document.getElementById('contact_bg').style.display = "block";
-    document.getElementById('contact').style.display = "block";
-}
-function closeContactForm(){
-    document.getElementById('contact_bg').style.opacity = "0.0";
-    document.getElementById('contact_bg').style.display = "none";
-    document.getElementById('contact').style.display = "none";
-}
-function clearContactErrors(){
-    document.getElementById('sender_name_error').style.opacity = "0";
-    document.getElementById('sender_email_error').style.opacity = "0";
-    document.getElementById('text_error').style.opacity = "0";
-}
-function contactSuccess(){
-    clearContactErrors();
-    document.getElementById('contact_sender_name').value = "";
-    document.getElementById('contact_sender_email').value = "";
-    document.getElementById('contact_text').value = "";
-    document.getElementById('contact_menu').style.display = "none";
-    document.getElementById('contact_progress').style.display = "none";
-    document.getElementById('contact_confirmation').style.display = "block";
-}
-function contactError(response){
-    document.getElementById('contact_menu').style.display = "block";
-    document.getElementById('contact_progress').style.display = "none";
-    document.getElementById('contact_confirmation').style.display = "none";
-    var errors = JSON.parse(response.replace(/\s/g, '')).response.errors
-    for (let i = 0; i < errors.length; i++) {
-        if (errors[i] === "INVALID_EMAIL"){
-            document.getElementById('sender_email_error').style.display = "inline";
-        }
-        if (errors[i] === "INVALID_TEXT"){
-            document.getElementById('text_error').style.display = "inline";
-        }
-    }
-    
-}
-function sendMessage(e) {
-    // Prevent form submit
-    if (e.preventDefault) e.preventDefault();
-
-    // Hide previously shown errors
-    clearContactErrors();
-
-    // Validate fields
-    var success = true;
-    var sender_name = document.getElementById('contact_sender_name').value;
-    var sender_email = document.getElementById('contact_sender_email').value;
-    var text = document.getElementById('contact_text').value;
-    
-    // Sender name is always OK
-    // Sender email has to look like an email
-    const res = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-    if (res.test(String(sender_email).toLowerCase()) === false){
-        success = false;
-        document.getElementById('sender_email_error').style.opacity = "1";
-    }
-    // Text needs to have something
-    if (text.length < 1){
-        success = false;
-        document.getElementById('text_error').style.opacity = "1";
-    }
-    if (success === false){
-        return false;
-    }
-    
-    // Send request
-    var url = "/contact/";
-    var xhr = new XMLHttpRequest();
-    xhr.open("POST", url, true);
-    //xhr.setRequestHeader("Content-Type", "application/json");
-    xhr.onreadystatechange = function () {
-        if (xhr.readyState === 4) {
-            if (xhr.status === 200){
-                contactSuccess();
-            }
-            else{
-                contactError(xhr.responseText)
-            }
-        }};
-    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
-    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
-    datastring =
-      'authenticity_token=' + encodeURI(document.getElementById('authenticity_token').value) +
-      '&sender_name=' + encodeURI(sender_name) +
-      '&sender_email=' + encodeURI(sender_email) +
-      '&text=' + encodeURI(text)
-    document.getElementById('contact_menu').style.display = "none";
-    document.getElementById('contact_progress').style.display = "block";
-    document.getElementById('contact_confirmation').style.display = "none";
-    xhr.send(datastring);
-
-
-    // You must return false to prevent the default form behavior
-    return false;
-}
-

+ 0 - 48
app/assets/javascripts/project.js

@@ -1,48 +0,0 @@
-// Populate this array as teh pages are loaded
-var images = [];
-selectedPhoto = 0;
-
-var galleryOpen = false;
-function closeGallery(){
-    document.getElementById('gallery_bg').style.opacity = "0.0";
-    document.getElementById('gallery_bg').style.display = "none";
-    document.getElementById('gallery').style.display = "none";
-    galleryOpen = false;
-}
-
-function showImage(index){
-    if (galleryOpen === false){
-        openGallery();
-    }
-    document.getElementById('gallery_image').src = images[index];
-    selectedPhoto = index;
-    var previews = document.getElementsByClassName('gallery_preview');
-    for (i = 0; i < previews.length; i ++){
-        previews[i].classList.remove('gallery_preview_selected');
-    }
-    document.getElementById('gallery_preview_' + index).classList.add('gallery_preview_selected')
-}
-
-
-function openGallery(){
-    document.getElementById('gallery_bg').style.opacity = "0.6";
-    document.getElementById('gallery_bg').style.display = "block";
-    document.getElementById('gallery').style.display = "block";
-    galleryOpen = true;
-}
-
-function previousPhoto(){
-    newIndex = selectedPhoto - 1;
-    if (newIndex < 0){
-        newIndex = images.length - 1;
-    }
-    showImage(newIndex);
-}
-
-function nextPhoto(){
-    newIndex = selectedPhoto + 1;
-    if (newIndex >= images.length){
-        newIndex = 0;
-    }
-    showImage(newIndex);
-}

+ 0 - 14
app/assets/stylesheets/application.css

@@ -1,14 +0,0 @@
-/*
- * This is a manifest file that'll be compiled into application.css, which will include all the files
- * listed below.
- *
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
- * vendor/assets/stylesheets directory can be referenced here using a relative path.
- *
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
- * files in this directory. Styles in this file should be added after the last require_* statement.
- * It is generally better to create a new file per style scope.
- *
- *= require_self
- */

+ 0 - 51
app/assets/stylesheets/help.css

@@ -1,51 +0,0 @@
-section{
-    padding: 0;
-    max-height: 2.8em;
-    overflow-y: hidden;
-    border-bottom-right-radius: 0.5em;
-    border-bottom-left-radius: 0.5em;
-    transition: all .5s ease-in-out;
-}
-@media screen and (max-width : 800px){
-    section{
-        max-height: 2em;
-        width: 90%;
-    }
-}
-section.selected{
-    max-height: 30em;
-    border-bottom-right-radius: 1.5em;
-    border-bottom-left-radius: 1.5em;
-}
-@media screen and (max-width : 800px){
-    section.selected{
-        max-height: 50em;
-    }
-}
-section h3{
-    margin: 0;
-}
-@media screen and (max-width : 800px){
-    section h3{
-        margin: -0.8em -1em 0em -1em;
-    }
-}
-section h3 img.slider{
-    width: 1em;
-    height: 1em;
-    transition: all .3s ease-in-out;
-    float: left;
-    margin-left: -2em;
-}
-@media screen and (max-width : 800px){
-    section h3 img.slider{
-        margin-left: 0;
-        margin-top: 0.1em;
-    }
-}
-section.selected h3 img.slider{
-    transform: rotate(90deg);
-}
-section p{
-    margin: 1em 2em;
-}

+ 0 - 3
app/assets/stylesheets/profiles.scss

@@ -1,3 +0,0 @@
-// Place all the styles related to the Profile controller here.
-// They will automatically be included in application.css.
-// You can use Sass (SCSS) here: https://sass-lang.com/

+ 0 - 4
app/channels/application_cable/channel.rb

@@ -1,4 +0,0 @@
-module ApplicationCable
-  class Channel < ActionCable::Channel::Base
-  end
-end

+ 0 - 4
app/channels/application_cable/connection.rb

@@ -1,4 +0,0 @@
-module ApplicationCable
-  class Connection < ActionCable::Connection::Base
-  end
-end

+ 0 - 32
app/controllers/application_controller.rb

@@ -1,32 +0,0 @@
-class ApplicationController < ActionController::Base
-    rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
-
-    before_action :set_locale
-    before_action :set_user
-    skip_before_action :verify_authenticity_token
-
-    private
-
-    def set_user
-        @user = User.find(1)
-    end
-    
-    def set_locale
-        I18n.locale = extract_locale || I18n.default_locale
-        Rails.application.reload_routes!
-    end
-
-    def extract_locale
-        parsed_locale = params[:locale]
-        I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
-    end
-    
-    def record_not_found(m, *args, &block)
-        Rails.logger.error("MMM" + m)
-        print("\nMETHOD MISSING\n")
-        redirect_to :controller=>"error", :action=>"error_404"
-        # or render/redirect_to somewhere else
-    end
-
-    
-end

+ 0 - 0
app/controllers/concerns/.keep


+ 0 - 53
app/controllers/contact_controller.rb

@@ -1,53 +0,0 @@
-class ContactController < ApplicationController
-    def index
-        @message = Message.new
-    end
-    def create
-        # Validate message fields
-        valid = true
-        @errors = []
-        # Name is irrelevant, don't vlaidate
-        # Email has to LOOK like an email
-        if !(request.POST["sender_email"].match(URI::MailTo::EMAIL_REGEXP))
-             valid = false
-             @errors.push("INVALID_EMAIL")
-        end
-        # Text needs to have something
-        if request.POST["text"].length < 1
-            valid = false
-            @error.push("INVALID_TEXT")
-        end
-
-        # If valid, save and send the email
-        if valid
-            @message = Message.new()
-            @message.user_id = @user.id
-            @message.sender_name = request.POST["sender_name"]
-            @message.sender_email = request.POST["sender_email"]
-            @message.text = request.POST["text"]
-            @message.dtime = Time.now
-            @message.language = I18n.locale
-            @message.ip = request.remote_ip
-            
-        
-            if @message.save
-                ApplicationMailer.message_email(@user, @message).deliver_later
-            else
-                print("Error saving message: \n")
-                print("   FROM: " + request.POST["sender_name"] + "(" + request.POST["sender_email"] + ")\n")
-                print("   TEXT: " + request.POST["text"] + ")\n")
-                print(@message.errors.full_messages)
-                print("\n")
-            end
-            respond_to do |format|
-                format.json { render 'contact/success'}
-            end
-        # If invalid, return the error
-        else
-            respond_to do |format|
-                format.json { render 'contact/error', status: 400}
-            end
-        end
-    end
-
-end

+ 0 - 5
app/controllers/error_controller.rb

@@ -1,5 +0,0 @@
-class ErrorController < ApplicationController
-    def error_404
-        print("\nERRORCONTROLLER\n")
-    end
-end

+ 0 - 4
app/controllers/help_controller.rb

@@ -1,4 +0,0 @@
-class HelpController < ApplicationController
-    def index
-    end
-end

+ 0 - 5
app/controllers/main_controller.rb

@@ -1,5 +0,0 @@
-class MainController < ApplicationController
-    def index
-        @projects = Project.where(user_id: @user.id).order(priority: :desc).limit(3)
-    end
-end

+ 0 - 6
app/controllers/profile_controller.rb

@@ -1,6 +0,0 @@
-class ProfileController < ApplicationController
-    def index
-        @resume_main = @user.resumes.where(user_id: @user.id, language: I18n.locale).order(priority: :desc)[0]
-        @resume_languages = @user.resumes.where(user_id: @user.id, priority: @resume_main.priority).where.not(language: I18n.locale)
-    end
-end

+ 0 - 16
app/controllers/projects_controller.rb

@@ -1,16 +0,0 @@
-class ProjectsController < ApplicationController
-    def index
-        @projects = Project.where(user_id: @user.id).order(priority: :desc)
-    end
-    def show
-        @project = Project.where(permalink: params[:permalink])[0]
-        if @project == nil
-            print("ERROR 404")
-            #render status: 404
-            #redirect_to '/404'
-            respond_to do |format|
-                format.all { render "error/error_404", status: 404}
-              end
-        end
-    end
-end

+ 0 - 2
app/helpers/application_helper.rb

@@ -1,2 +0,0 @@
-module ApplicationHelper
-end

+ 0 - 2
app/helpers/error_helper.rb

@@ -1,2 +0,0 @@
-module ErrorHelper
-end

+ 0 - 2
app/helpers/help_helper.rb

@@ -1,2 +0,0 @@
-module HelpHelper
-end

+ 0 - 2
app/helpers/profile_helper.rb

@@ -1,2 +0,0 @@
-module ProfileHelper
-end

+ 0 - 2
app/helpers/projects_helper.rb

@@ -1,2 +0,0 @@
-module ProjectsHelper
-end

+ 0 - 6
app/javascript/channels/consumer.js

@@ -1,6 +0,0 @@
-// Action Cable provides the framework to deal with WebSockets in Rails.
-// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command.
-
-import { createConsumer } from "@rails/actioncable"
-
-export default createConsumer()

+ 0 - 5
app/javascript/channels/index.js

@@ -1,5 +0,0 @@
-// Load all the channels within this directory and all subdirectories.
-// Channel files must be named *_channel.js.
-
-const channels = require.context('.', true, /_channel\.js$/)
-channels.keys().forEach(channels)

+ 0 - 13
app/javascript/packs/application.js

@@ -1,13 +0,0 @@
-// This file is automatically compiled by Webpack, along with any other files
-// present in this directory. You're encouraged to place your actual application logic in
-// a relevant structure within app/javascript and only use these pack files to reference
-// that code so it'll be compiled.
-
-import Rails from "@rails/ujs"
-import Turbolinks from "turbolinks"
-import * as ActiveStorage from "@rails/activestorage"
-import "channels"
-
-Rails.start()
-Turbolinks.start()
-ActiveStorage.start()

+ 0 - 7
app/jobs/application_job.rb

@@ -1,7 +0,0 @@
-class ApplicationJob < ActiveJob::Base
-  # Automatically retry jobs that encountered a deadlock
-  # retry_on ActiveRecord::Deadlocked
-
-  # Most jobs are safe to ignore if the underlying records are no longer available
-  # discard_on ActiveJob::DeserializationError
-end

+ 0 - 11
app/mailers/application_mailer.rb

@@ -1,11 +0,0 @@
-class ApplicationMailer < ActionMailer::Base
-    layout 'mailer'
-    default from: Rails.application.credentials.smtp[:username]
-   
-    def message_email(user, message)
-        @message = message
-        to = user.mail_adresses[0].address
-        to = Rails.application.credentials.smtp[:username]
-        mail(to: to, subject: 'New message')
-    end
-end

+ 0 - 3
app/models/application_record.rb

@@ -1,3 +0,0 @@
-class ApplicationRecord < ActiveRecord::Base
-  self.abstract_class = true
-end

+ 0 - 0
app/models/concerns/.keep


+ 0 - 4
app/models/license.rb

@@ -1,4 +0,0 @@
-class License < ApplicationRecord
-    has_many :project
-    has_one_attached :logo
-end

+ 0 - 3
app/models/mail_adress.rb

@@ -1,3 +0,0 @@
-class MailAdress < ApplicationRecord
-    belongs_to :user
-end

+ 0 - 5
app/models/message.rb

@@ -1,5 +0,0 @@
-class Message < ApplicationRecord
-    belongs_to :user
-    validates :sender_email, presence: true
-    validates :text, presence: true, length: { minimum: 10 }
-end

+ 0 - 19
app/models/project.rb

@@ -1,19 +0,0 @@
-class Project < ApplicationRecord
-    has_one_attached :logo
-    has_and_belongs_to_many :technologies
-    has_many :projectUrls
-    belongs_to :license
-    has_many_attached :images
-    def i18n_title
-        return Text.where(user_id: self.user_id, language: I18n.locale, key: self.title)[0].text
-    end
-    def i18n_header
-        return Text.where(user_id: self.user_id, language: I18n.locale, key: self.header)[0].text
-    end
-    def i18n_text
-        return Text.where(user_id: self.user_id, language: I18n.locale, key: self.text)[0].text
-    end
-    def i18n_comment
-        return Text.where(user_id: self.user_id, language: I18n.locale, key: self.comment)[0].text
-    end
-end

+ 0 - 4
app/models/project_url.rb

@@ -1,4 +0,0 @@
-class ProjectUrl < ApplicationRecord
-      belongs_to :project
-      belongs_to :urlTarget
-end

+ 0 - 4
app/models/resume.rb

@@ -1,4 +0,0 @@
-class Resume < ApplicationRecord
-    belongs_to :user
-    has_one_attached :file
-end

+ 0 - 6
app/models/social_link.rb

@@ -1,6 +0,0 @@
-class SocialLink < ApplicationRecord
-    has_one_attached :logo
-    def i18n_link_title
-        return Text.where(language: I18n.locale, key: self.link_title)[0].text
-    end
-end

+ 0 - 5
app/models/technology.rb

@@ -1,5 +0,0 @@
-class Technology < ApplicationRecord
-    has_one_attached :logo
-    has_and_belongs_to_many :users
-    has_and_belongs_to_many :projects
-end

+ 0 - 3
app/models/text.rb

@@ -1,3 +0,0 @@
-class Text < ApplicationRecord
-    belongs_to :user, optional: true
-end

+ 0 - 9
app/models/url_target.rb

@@ -1,9 +0,0 @@
-class UrlTarget < ApplicationRecord
-    has_one_attached :logo
-    def i18n_name
-        return Text.where(language: I18n.locale, key: self.name)[0].text
-    end
-    def i18n_summary
-        return Text.where(language: I18n.locale, key: self.summary)[0].text
-    end
-end

+ 0 - 13
app/models/user.rb

@@ -1,13 +0,0 @@
-class User < ApplicationRecord
-    has_many :mail_adresses
-    has_many :resumes
-    has_many_attached :picture
-    has_and_belongs_to_many :technologies
-    has_many :user_social_links
-    def i18n_tagline
-        return Text.where(user_id: self.id, language: I18n.locale, key: self.tagline)[0].text
-    end
-    def i18n_bio
-        return Text.where(user_id: self.id, language: I18n.locale, key: self.bio)[0].text
-    end
-end

+ 0 - 7
app/models/user_social_link.rb

@@ -1,7 +0,0 @@
-class UserSocialLink < ApplicationRecord
-    belongs_to :user
-    belongs_to :social_link
-    def generate_link
-        return self.social_link.pattern.sub("*", self.link)
-    end
-end

+ 0 - 26
app/views/application_mailer/message_email.html.erb

@@ -1,26 +0,0 @@
-<div>
-    New message recieved on the website:
-    <ul>
-        <li>
-            <span>
-                From:
-            </span>
-            <%= @message.sender_name %>
-        </li>
-        <li>
-            <span>
-                eMail:
-            </span>
-            <%= @message.sender_email %>
-        </li>
-        <li>
-            <span>
-                Time:
-            </span>
-            <%= @message.dtime %>
-        </li>
-    </ul>
-</div>
-<pre>
-    <%= @message.text %>
-</pre>

+ 0 - 18
app/views/contact/error.erb

@@ -1,18 +0,0 @@
-{
-    "response": {
-        "status": "ERROR",
-        "errors": [
-            <%
-                i = 0
-                max = @errors.length
-                @errors.each do |error|
-            %>
-                    "<%= error %>"
-                    <% if (i + 1) < max %>
-                        ,
-                    <% end %>
-                    <% i = i + 1 %>
-            <% end %>
-        ]
-    }
-}

+ 0 - 5
app/views/contact/success.erb

@@ -1,5 +0,0 @@
-{
-  "response": {
-    "status": "SUCCESS"
-  }
-}

+ 0 - 31
app/views/error/error_404.html.erb

@@ -1,31 +0,0 @@
-<% content_for :head do %>
-    <%= stylesheet_link_tag "error", media: "all", "data-turbolinks-track": "reload" %>
-<% end %>
-<section>
-    <h3>
-        <%= t("error.error_404_title" ) %>
-    </h3>
-    <div id='left'>
-        <p id='code'>
-            404
-        </p>
-        <p>
-            <span><%= t("error.error" ) %></span>
-            <span><%= t("error.code" ) %></span>
-        </p>
-    </div>
-    <div id='right'>
-        <h4>
-           <%= t("error.error_404_description" ) %>
-        </h4>
-        <%= t("error.try_solution" ) %>
-        <ul>
-            <li>
-                <a href='javascript: history.go(-1);'><%= t("error.error_400_solution_0") %></a>
-            </li>
-            <li>
-                <a href='/'><%= t("error.error_400_solution_1") %></a>
-            </li>
-        </ul>
-    </div>
-</section>

+ 0 - 41
app/views/help/index.html.erb

@@ -1,41 +0,0 @@
-<% content_for :head do %>
-    <%= stylesheet_link_tag "help", media: "all", "data-turbolinks-track": "reload" %>
-    <%= javascript_include_tag "help", "data-turbolinks-track" => true  %>
-<% end %>
-<section id='about' class='help'>
-    <h3 class='pointer' onClick='toggleHelp("about");'>
-        <%= image_tag("control/slid_r.svg", class: "slider", alt: t("help.expand")) %>
-        <%= t("help.about_title") %>
-    </h3>
-    <p>
-        <%= t("help.about_text").html_safe %>
-    </p>
-</section>
-<section id='privacy' class='help'>
-    <h3 class='pointer' onClick='toggleHelp("privacy");'>
-        <%= image_tag("control/slid_r.svg", class: "slider", alt: t("help.expand")) %>
-        <%= t("help.privacy_title") %>
-    </h3>
-    <p>
-        <%= t("help.privacy_text").html_safe %>
-    </p>
-</section>
-<section id='cookies' class='help'>
-    <h3 class='pointer' onClick='toggleHelp("cookies");'>
-        <%= image_tag("control/slid_r.svg", class: "slider", alt: t("help.expand")) %>
-        <%= t("help.cookies_title") %>
-    </h3>
-    <p>
-        <%= t("help.cookies_text").html_safe %>
-    </p>
-</section>
-<section id='license' class='help'>
-    <h3 class='pointer' onClick='toggleHelp("license");'>
-        <%= image_tag("control/slid_r.svg", class: "slider", alt: t("help.expand")) %>
-        <%= t("help.license_title") %>
-    </h3>
-    <p>
-        <%= t("help.license_text").html_safe %>
-    </p>
-</section>
-

+ 0 - 90
app/views/layouts/_contact_form.html.erb

@@ -1,90 +0,0 @@
-<div id='contact_bg'>
-</div>
-<section id='contact' >
-    <h3>
-        <%= t("contact.title") %>
-        <span id='contact_close'>
-            <%=
-                link_to(
-                    image_tag(
-                        "control/close.svg", 
-                        id: "contact_close",
-                        alt: t("contact.cancel")
-                    ),
-                    #"mailto:" + @user.mail_adresses[0].address,
-                    "javascript:closeContactForm()"
-                )
-            %>
-        </span>
-    </h3>
-    <div id='contact_menu'>
-        <form id='contact_form' action='/<%=I18n.locale%>/contact/' method='post'>
-            <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
-            <table>
-                <tr>
-                    <td class='label'>
-                        <%= t("contact.name") %>
-                    </td>
-                    <td class='input'>
-                        <input type='text' name='sender_name' id='contact_sender_name'/>
-                        <br/>
-                        <span class='contact_error' id='sender_name_error'>
-                        </span>
-                    </td>
-                </tr>
-                <tr>
-                    <td class='label'>
-                        <%= t("contact.mail") %>
-                    </td>
-                    <td class='input'>
-                        <input type='text' name='sender_email' id='contact_sender_email'/>
-                        <br/>
-                        <span class='contact_error' id='sender_email_error'>
-                            <%= t('contact.error.invalid_email') %>
-                        </span>
-                    </td>
-                </tr>
-                <tr>
-                    <td class='label'>
-                        <%= t("contact.message") %>
-                    </td>
-                    <td class='input'>
-                        <textarea type='text' name='text' id='contact_text'></textarea>
-                        <br/>
-                        <span class='contact_error' id='text_error'>
-                            <%= t('contact.error.empty_text') %>
-                        </span>
-                    </td>
-                </tr>
-            </table>
-            <div class='contact_buttons'>
-                <input type='button' value='<%=t("contact.cancel")%>' onClick='closeContactForm();'>
-                <input type='submit' value='<%=t("contact.send")%>'>
-            </div>
-        </form>
-    </div>
-    <div id='contact_progress'>
-        <%=
-            image_tag(
-                "social/email.svg", 
-                alt: ""
-            )
-        %>
-    </div>
-    <div id='contact_confirmation'>
-        <p>
-            <%= t('contact.confirmation') %>
-        </p>
-        <div class='contact_buttons'>
-            <input type='button' value='<%=t("contact.close")%>' onClick='closeContactForm();'>
-        </div>
-    </div>
-    <script>
-        var form = document.getElementById('contact_form');
-        if (form.attachEvent) {
-            form.attachEvent("submit", sendMessage);
-        } else {
-            form.addEventListener("submit", sendMessage);
-        }
-    </script>
-</section>

+ 0 - 72
app/views/layouts/_footer.html.erb

@@ -1,72 +0,0 @@
-<footer>
-    <hr id='footer_separator'/>
-    <table id='footer_table'>
-        <tr>
-            <td id='footer_left'>
-                <span id='footer_follow' class='desktop'>
-                    <%= t("footer.follow") %>
-                </span>
-                <%=
-                    link_to(
-                        image_tag(
-                            "social/email.svg",
-                            class: "footer_social_icon",
-                            alt: t("social.mail")
-                        ),
-                        "javascript:showContactForm();",
-                        title: + t("index.email")
-                    )
-                %>
-                <% @user.user_social_links.each do |link| %>
-                    <%=
-                        link_to(
-                            image_tag(
-                                link.social_link.logo,
-                                class: "footer_social_icon",
-                                alt: link.social_link.i18n_link_title
-                            ),
-                            link.generate_link,
-                            target: "_blank",
-                            title: link.social_link.i18n_link_title
-                        )
-                    %>
-                <% end %>
-            </td>
-            <td id='footer_center'>
-                <%= t("footer.copy", name: @user.full_name, year: Date.today.year) %>
-            </td>
-            <td id='footer_right'>
-                <%= link_to(
-                    t("footer.help"),
-                    help_path,
-                    id: "footer_help",
-                    title: t("footer.help")
-                ) %>
-                <br/><br/>
-                <%
-                    path = request.fullpath
-                    if path.to_s[0..4] =~ /[a-z]{2}/ then
-                        path = path[3..] + "/"
-                    end
-                %>
-                <% I18n.available_locales.each do |locale| %>
-                    <%=
-                        link_to(
-                            image_tag(
-                                "lang/#{locale.to_s}.svg",
-                                alt: t("footer.lang_#{locale.to_s}")
-                            ),
-                            request.protocol +
-                                request.host_with_port +
-                                "/" +
-                                locale.to_s +
-                                path,
-                            class: "lang",
-                            title: t("footer.lang_#{locale.to_s}")
-                        )
-                    %>
-                <% end %>
-            </td>
-        </tr>
-    </table>
-</footer>

+ 0 - 34
app/views/layouts/_header.html.erb

@@ -1,34 +0,0 @@
-<header>
-    <div id='logo'>
-        inigoValentin
-    </div>
-    <nav>
-        <%=
-            link_to(
-                image_tag(
-                    "icon/home.svg",
-                    alt: ""
-                ) + t('header.home'),
-                main_path()
-            )
-        %>
-        <%=
-            link_to(
-                image_tag(
-                    "icon/profile.svg",
-                    alt: ""
-                ) + t('header.profile'),
-                profile_path()
-            )
-        %>
-        <%=
-            link_to(
-                image_tag(
-                    "icon/project.svg",
-                    alt: ""
-                ) + t('header.projects'),
-                project_index_url()
-            )
-        %>
-    </nav>
-</header>

+ 0 - 22
app/views/layouts/application.html.erb

@@ -1,22 +0,0 @@
-<!DOCTYPE html>
-<html lang='<%=I18n.locale%>'>
-    <head>
-        <title>InigoValentin</title>
-        <meta name='viewport' content='width=device-width,initial-scale=1'>
-        <%= csrf_meta_tags %>
-        <%= csp_meta_tag %>
-
-        <%= stylesheet_link_tag "ui", media: "all", "data-turbolinks-track": "reload" %>
-        <%= javascript_include_tag "main", "data-turbolinks-track" => true  %>
-        <%= yield(:head) %>
-    </head>
-
-    <body>
-        <%= render "layouts/header" %>
-        <main>
-            <%= yield %>
-        </main>
-        <%= render "layouts/footer" %>
-        <%= render "layouts/contact_form"%>
-  </body>
-</html>

+ 0 - 18
app/views/layouts/mailer.html.erb

@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-        <style>
-            ul#details span{
-                font-weight: bold;
-            }
-            pre{
-                background-color: #333333;
-            }
-        </style>
-    </head>
-
-    <body>
-        <%= yield %>
-    </body>
-</html>

+ 0 - 1
app/views/layouts/mailer.text.erb

@@ -1 +0,0 @@
-<%= yield %>

+ 0 - 85
app/views/main/index.html.erb

@@ -1,85 +0,0 @@
-<% content_for :head do %>
-    <%= stylesheet_link_tag "main", media: "all", "data-turbolinks-track": "reload" %>
-<% end %>
-<section id='profile'>
-    <h3>
-        <%= @user.full_name %>
-    </h3>
-    <article>
-        <%= image_tag(@user.picture[0], id: "profile_image", alt: "")%>
-        <h4 id='tagline'>
-            <%= @user.i18n_tagline %>
-        </h4>
-        <p id='bio'>
-            <%= @user.i18n_bio %>
-        </p>
-        <div class='buttons'>
-            <%=
-                link_to(
-                    image_tag(
-                        "social/email.svg", 
-                        class: "footer_social_icon",
-                        alt: t("social.mail")
-                    ) + t("index.email"),
-                    "javascript:showContactForm()",
-                    class: "a_button"
-                )
-            %>
-            <%=
-                link_to(
-                    t("index.profile_more"),
-                    profile_path(),
-                    class: "a_button"
-                )
-            %>
-        </div>
-    </article>
-</section>
-<section id='projects'>
-    <h3>
-        <%= t("index.projects") %>
-    </h3>
-    <% @projects.each do |project| %>
-        <article class='project'>
-            <table>
-                <tr>
-                    <td class='icon'>
-                        <% if project.logo.attached? %>
-                            <%=
-                                link_to(
-                                    image_tag(
-                                        project.logo,
-                                        class: "project_image",
-                                        alt: project.i18n_title
-                                    ),
-                                    project_show_path(permalink: project.permalink)
-                            ) %>
-                        <% end %>
-                    </td>
-                    <td class='project_details'>
-                        <h4 class='project_name'>
-                            <%=
-                                link_to(
-                                    project.i18n_title,
-                                    project_show_path(permalink: project.permalink)
-                                )
-                            %>
-                        </h4>
-                        <span>
-                            <%= project.i18n_header %>
-                        </span>
-                    </td>
-                </tr>
-            </table>
-        </article>
-    <% end %>
-    <div class='buttons'>
-        <%=
-            link_to(
-                t("index.projects_all"),
-                project_index_url(),
-                class: "a_button"
-            )
-        %>
-    </div>
-</section>

+ 0 - 64
app/views/profile/index.html.erb

@@ -1,64 +0,0 @@
-<% content_for :head do %>
-    <%= stylesheet_link_tag "profile", media: "all", "data-turbolinks-track": "reload" %>
-<% end %>
-<section id='info'>
-    <h3>
-        <%= @user.full_name %>
-    </h3>
-    <article>
-        <h5>
-            <%=image_tag(@user.picture[0], id: "profile_image", alt: "")%>
-            <span>
-                <%= @user.i18n_tagline %>
-            </span>
-        </h5>
-        <div id='social'>
-            <% @user.user_social_links.each do |link| %>
-                <%= link_to(
-                    image_tag(link.social_link.logo, class: "project_image", alt: link.social_link.i18n_link_title, title: link.social_link.i18n_link_title),
-                    link.generate_link,
-                    target: "_blank",
-                    title: link.social_link.i18n_link_title
-                ) %>
-            <% end %>
-        </div>
-    </article>
-</section>
-<section id='about'>
-    <h3>
-        <%= t("profile.description") %>
-    </h3>
-    <article>
-        <p>
-            <%= @user.i18n_bio %>
-        </p>
-        <div id='resume_download'>
-            <%= link_to(
-                t("profile.resume"),
-                @resume_main.file,
-                target: "_blank",
-                title: t("profile.resume"),
-                class: "a_button"
-            ) %>
-            <% if @resume_languages.size > 0 %>
-                <details>
-                    <summary class='pointer'>
-                        <%= t("profile.resume_lang") %>
-                    </summary>
-                    <ul>
-                        <% @resume_languages.each do |resume| %>
-                            <li>
-                                <%= link_to(
-                                    t("profile.resume_" + resume.language),
-                                    resume.file,
-                                    target: "_blank",
-                                    title: t("profile.resume_" + resume.language)
-                                ) %>
-                            </li>
-                        <% end%>
-                    </ul>
-                </details>
-            <% end%>
-        </div>
-    </article>
-</section>

+ 0 - 43
app/views/projects/index.html.erb

@@ -1,43 +0,0 @@
-<% content_for :head do %>
-    <%= stylesheet_link_tag "projects", media: "all", "data-turbolinks-track": "reload" %>
-<% end %>
-<section>
-    <h3>
-        <%= t("project.title") %>
-    </h3>
-    <% @projects.each do |project| %>
-        <article class='project'>
-            <table>
-                <tr>
-                    <td class='logo'>
-                        <% if project.logo.attached? %>
-                            <%=
-                                link_to(
-                                    image_tag(
-                                        project.logo,
-                                        class: "project_image",
-                                        alt: project.i18n_title
-                                    ),
-                                    project_show_path(permalink: project.permalink)
-                                )
-                            %>
-                        <% end %>
-                    </td>
-                    <td class='project_details'>
-                        <h4 class='project_name'>
-                            <%=
-                                link_to(
-                                    project.i18n_title,
-                                    project_show_path(permalink: project.permalink)
-                                )
-                            %>
-                        </h4>
-                        <span>
-                            <%= project.i18n_header %>
-                        </span>
-                    </td>
-                </tr>
-            </table>
-        </article>
-    <% end %>
-</section>

+ 0 - 155
app/views/projects/show.html.erb

@@ -1,155 +0,0 @@
-<% content_for :head do %>
-    <%= stylesheet_link_tag "project", media: "all", "data-turbolinks-track": "reload" %>
-    <%= javascript_include_tag "project", "data-turbolinks-track" => true  %>
-<% end %>
-<section>
-    <h3>
-        <%= @project.i18n_title %>
-    </h3>
-    <article>
-        <div id='info_summary'>
-            <h4>
-                <%= @project.i18n_header %>
-            </h4>
-            <div id='text'>
-                <% if @project.logo.attached? %>
-                    <%=
-                        image_tag(
-                            @project.logo,
-                            class: "project_image",
-                            id: "project_image",
-                            alt: ""
-                        )
-                    %>
-                <% end %>
-                <%= @project.i18n_text.html_safe %>
-            </div>
-            <% if @project.comment != nil %>
-                <div id='comment'>
-                    <div id='comment_title'>
-                        <%= t("project.comment", name: @user.first_name) %>
-                    </div>
-                    <%= @project.i18n_comment.html_safe %>
-                </div>
-            <% end %>
-        </div>
-        <div id='info_details'>
-            <div id='links' class='details'>
-                <% @project.projectUrls.each do |url| %>
-                    <%=
-                        link_to(
-                            image_tag(
-                                url.urlTarget.logo,
-                                class: "link",
-                                title: url.urlTarget.i18n_summary,
-                                alt: url.urlTarget.i18n_summary
-                            ) + url.urlTarget.i18n_name,
-                            url.url,
-                            class: "link",
-                            target: "_blank",
-                            title: url.urlTarget.i18n_summary
-                    ) %>
-                <% end %>
-            </div>
-            <div id='technologies' class='details'>
-                <% @project.technologies.each do |technology| %>
-                    <span class='technology technology_<%=technology.nature%>'>
-                        <%= image_tag(technology.logo, class: "technology_logo", alt: "") %>
-                        <span class='technology_name'>
-                            <%= technology.name %>
-                        </span>
-                    </span>
-                <% end %>
-            </div>
-            <div id='license' class='details'>
-                <%= t("project.license") %>:
-                <%=
-                    link_to(
-                        image_tag(
-                            @project.license.logo,
-                            title: @project.license.name,
-                            alt: @project.license.name
-                        ),
-                        @project.license.url,
-                        target: "_blank",
-                        title: @project.license.name
-                    )
-                %>
-            </div> <!-- #license -->
-            <% if @project.images.attached? %>
-                <div id='images' class='details'>
-                    <%= t("project.images") %>
-                    <br/>
-                    <% i = 0 %>
-                    <% @project.images.each do |image| %>
-                        <%=
-                            link_to(
-                                image_tag(
-                                    image,
-                                    class: "project_image",
-                                    alt: ""
-                                ),
-                                "javascript:showImage(" + i.to_s + ");"
-                        ) %>
-                        <script>
-                            images[<%= i %>] = '<%= url_for(image) %>';
-                        </script>
-                        <% i = i + 1 %>
-                    <% end %>
-                </div>
-            <% end %>
-        </div> <!-- #info_details -->
-    </article>
-</section>
-<div id='gallery_bg'>
-</div>
-<section id='gallery'>
-    <h3>
-        <%= t("project.gallery_title", project: @project.i18n_title) %>
-        <span id='gallery_close'>
-            <%=
-                link_to(
-                    image_tag(
-                        "control/close.svg", 
-                        id: "gallery_close",
-                        alt: t("contact.cancel")
-                    ),
-                    #"mailto:" + @user.mail_adresses[0].address,
-                    "javascript:closeGallery()"
-                )
-            %>
-        </span>
-    </h3>
-    <article>
-        <div id='gallery_image_container'>
-            <img id='gallery_image'/>
-        </div>
-        <div id='gallery_controls'>
-            <div class='gallery_control' id='gallery_control_left'>
-                <input type='button' value='<' onClick='javascript:previousPhoto();'/>
-            </div>
-            <div id='image_list'>
-                <% i = 0 %>
-                <% @project.images.each do |image| %>
-                    <%=
-                        link_to(
-                            image_tag(
-                                image,
-                                id: "gallery_preview_" + i.to_s,
-                                class: "gallery_preview",
-                                alt: ""
-                            ),
-                            "javascript:showImage(" + i.to_s + ");"
-                    ) %>
-                    <script>
-                        images[<%= i %>] = '<%= url_for(image) %>';
-                    </script>
-                    <% i = i + 1 %>
-                <% end %>
-            </div>
-            <div class='gallery_control' id='gallery_control_right'>
-                <input type='button' value='>' onClick='javascript:nextPhoto();'/>
-            </div>
-        </div>
-    </article>
-</section>

+ 26 - 0
application/Application.php

@@ -0,0 +1,26 @@
+<?php
+/**
+ * Constants file.
+ *
+ * Provides some predefined values.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+    class URL{
+        //static $BASE = Net.get_protocol() . $_SERVER["HTTP_HOST"];
+        //static $STATIC = Net.get_protocol() . $_SERVER["HTTP_HOST"];
+        //static $BASE = ((1 == 1) ? 1 : 0);
+        //static $BASE = $_SERVER["HTTP_HOST"];
+    }
+    
+    final class EXCEPTION_CODE{
+        const PDO_UNSUPPORTED = 800001;
+        const PDO_MYSQL_HOSTNAME = 807701;
+        const PDO_MYSQL_DBNAME = 807702;
+        const PDO_MYSQL_PORT = 807703;
+    }
+
+?>

+ 107 - 0
application/Context.php

@@ -0,0 +1,107 @@
+<?php
+/**
+ * Context file.
+ *
+ * Provides a handy class with usefull data to be accessible from anywhere in the app.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+require_once(PATH::ENTITY . "User.php");
+
+/**
+ * Application context.
+ *
+ * Stores usefull data to be retrieved anywhere in the app.
+ *
+ * @category Context
+ */
+class Context {
+
+    /**
+     * @var SQLITE3 Database connection.
+     */
+    private $db;
+
+    /**
+     * Current site user profile.
+     */
+    private $user;
+    
+    /**
+     * Two letter language code.
+     */
+    private $lang;
+    
+    /**
+     * List of two letter language codes available in the application.
+     * */
+    private $available_languages = [];
+    
+    public function __construct(){
+        // TODO: Validate language codes.
+        $langs = parse_ini_file(__DIR__ . "/config/config.ini", true)["language"]["available"];
+        $this->available_languages = explode("|", $langs);
+        $this->lang = parse_ini_file(__DIR__ . "/config/config.ini", true)["language"]["default"];
+    }
+
+    /**
+     * Retrieves the database connection.
+     *
+     * @return SQLITE3 The database connection.
+     */
+    public function get_db(){
+        return $this->db;
+    }
+
+    /**
+     * Sets the database connection
+     *
+     * @param SQLITE3 Database connection.
+     */
+    protected function set_db($db){
+        $this->db = $db;
+        Log::debug("Context database is set and accesible.");
+        if ($this->db == null){
+            Log::error("DB is null");
+        }
+    }
+
+    public function get_user(){
+        if ($this->user == null) $this->user = new User();
+        return $this->user;
+    }
+    
+    /**
+     * Retrieves the language.
+     * 
+     * @return string Two letter language code.
+     */
+    public function get_lang(){
+        return $this->lang;
+    }
+    
+    /**
+     * Sets the anguage.
+     * @param string $code Two letter language code.
+     */
+    public function set_lang($code){
+        if (in_array(strtolower($code), $this->available_languages)){
+            $this->lang = strtolower($code);
+            setcookie("lang", $this->lang, time()+ 60 * 60 * 24 * 30, "/", $_SERVER["HTTP_HOST"]);
+        }
+        else Log::warn("Tried to set unsupported language '$code'");
+    }
+    
+    /**
+     * Retrieves the list of available languages.
+     *
+     * @return string Two letter language code of avilable languages.
+     */
+    public function get_available_langs(){
+        return $this->available_languages;
+    }
+
+}

+ 207 - 0
application/Controller.php

@@ -0,0 +1,207 @@
+<?php
+/**
+ * Controller file.
+ *
+ * Provides a class to handle all posible request.
+ * 
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IVV
+ */
+
+require_once(__DIR__ . "/helper/DB_Helper.php");
+require_once(__DIR__ . "/helper/TEXT_Helper.php");
+require_once(__DIR__ . "/helper/NET_Helper.php");
+require_once(__DIR__ . "/helper/HTML_Helper.php");
+require_once(__DIR__ . "/util/Log.php");
+require_once(__DIR__ . "/util/Path.php");
+require_once(__DIR__ . "/util/URL.php");
+require_once(__DIR__ . "/Context.php");
+
+/**
+ * Application controller.
+ *
+ * Handles every request, creating the required models and selecting the
+ * view.
+ *
+ * @category Controller
+ */
+class Controller extends Context{
+
+    /**
+     * @var string[] GET parameters received by the controller.
+     */
+    private $params = [];
+
+    /**
+     * @var Context Publicly available context.
+     */
+    private $context;
+    
+    /**
+     * @var string URL command (first parameter, excluding optional language).
+     */
+     private $command = "";
+    
+     /**
+      * @var int HTTP status.
+      */
+    private $status = 201;
+
+    /**
+     * @var string HTTP message.
+     */
+    private $message = "OK";
+    
+
+    /**
+     * Retrieves the application context.
+     * 
+     * @return Context The context.
+     */
+    public function get_context(){return $this->context;}
+    
+    /**
+     * Controller constructor.
+     *
+     * Initializes a database connection and sets up the public context.
+     */
+    public function __construct(){
+        $this->context = new Context();
+        session_start();
+        $configuration = parse_ini_file(__DIR__ . "/config/config.ini", true);
+        $this->db = DB::start($configuration["database"]);
+        $this->context->set_db($this->db);
+        Log::debug("Controller created succesfully");
+    }
+    
+    private function execute_action($action){
+        require_once(PATH::ACTION . $action . ".php");
+        action();
+        return;
+    }
+    
+    /**
+     * Handles the request parameters.
+     *
+     * Receives the request params and gets itself ready. Authenticates the user (if needed)
+     * and validates the URL.
+     *
+     * @param string[] $params GET parameters of the request.
+     */
+    public function prepare($params){
+
+        // Parse parameters.
+        foreach($params as $p){
+            if (strlen($p) > 0){
+                Log::debug("Controller parameter: $p");
+                array_push($this->params, $p);
+            }
+        }
+        
+        // Check if the first parameter is a language code.
+        if (sizeof($this->params) > 0 && $this->context->get_user()->is_valid_language($this->params[0])){
+            $this->context->set_lang($this->params[0]);
+            $this->params = array_slice($this->params, 1);
+        }
+        elseif(isSet($_COOKIE['lang']) && $this->context->get_user()->is_valid_language($_COOKIE['lang'])){
+            $this->context->set_lang($_COOKIE['lang']);
+        }
+        else{
+            $lang = "";
+            // Set a default
+            if (sizeof($this->context->get_user()->get_langs()) > 0)
+                $lang = $this->context->get_user()->get_langs()[0]->get_code();
+            // Get a list of avaiable languages from client browser.
+            foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $accepted) {
+                $code = substr($accepted, 0, 2);
+                if ($this->context->get_user()->is_valid_language($code)){
+                    $lang = $code;
+                    break;
+                }
+            }
+            if ($lang != "") $this->context->set_lang($lang);
+        }
+        
+        // Obtain the command and process it
+        if (sizeof($this->params) > 0) $this->command = strtoupper($this->params[0]);
+        Log::debug("Controller command set: '" . $this->command . "'");
+    }
+    
+    /**
+     * Executes the required action.
+     *
+     * Must be called after {@see Controller::prepare()}.
+     * @return void|number
+     */
+    public function action(){
+        Log::debug("Executing action");
+        // If there was an error in preparation, it's time to throw it.
+        if ($this->status >= 400 && $this->status < 500){
+            Log::debug("Executing action error code: " . $this->status);
+            require_once(PATH::PAGE . "Error_Page.php");
+            $page = new Error_Page($this->status, $this->message);
+            require_once($page->get_view());
+            http_response_code($this->status);
+            return $this->status;
+        }
+        
+        // Process the command
+        $page = null;
+        switch ($this->command){
+            case "": // Main page
+                require_once(PATH::PAGE . "Home_Page.php");
+                $page = new Home_Page();
+                break;
+            case "PROJECT": // Projects
+            case "PROJECTS": // Projects
+                if (sizeof($this->params) > 1){
+                    require_once(PATH::PAGE . "Project_Page.php");
+                    $page = new Project_Page($this->params[1]);
+                }
+                else{
+                    require_once(PATH::PAGE . "Projects_Page.php");
+                    $page = new Projects_Page();
+                }
+                break;
+            case "PROFILE": // Profile
+                require_once(PATH::PAGE . "Profile_Page.php");
+                $page = new Profile_Page();
+                break;
+            case "HELP": // Help static page
+                require_once(PATH::PAGE . "Help_Page.php");
+                $page = new Help_Page();
+                break;
+            case "SITEMAP.XML": // Sitemap file
+                require_once(PATH::PAGE . "Sitemap_Page.php");
+                $page = new Sitemap_Page();
+                break;
+            case "ROBOTS.TXT": // robots.txt file
+                require_once(PATH::PAGE . "Robots_Page.php");
+                $page = new Robots_Page();
+                break;
+        }
+        
+        // If page is not set, it's a not found.
+        if ($page == null){
+            require_once(PATH::PAGE . "Error_Page.php");
+            $page = new Error_Page(404, "Page not found");
+        }
+        
+        // Once the page is loaded, chack if it can be seen
+        // TODO verify
+        if ($page->get_code() >= 400 && $page->get_code() < 600){
+            require_once(PATH::PAGE . "Error_Page.php");
+            $page = new Error_Page($page->get_code(), $page->get_message());
+            http_response_code($page->get_code());
+        }
+        
+        
+        // Load the view, or set an error code.
+        Log::debug("Loading view: " . $page->get_view());
+        require_once($page->get_view());
+        http_response_code($page->get_code());
+    }
+
+}
+?>

+ 22 - 0
application/config/config.ini

@@ -0,0 +1,22 @@
+[database]
+type = "mysql"
+host = "localhost"
+port = "3306"
+name = "web"
+user = "web"
+pass = "XXX"
+
+[language]
+available = "en|es|eu"
+default = "es"
+
+[user]
+id = "1"
+year = "2022"
+
+[log]
+level = "warn"
+file = "/var/log/"
+
+[maintenance]
+enable = false

+ 84 - 0
application/entity/Cv.php

@@ -0,0 +1,84 @@
+<?php
+/**
+ * CV (resume) entity file.
+ *
+ * Provides the entity Cv.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IVV
+ */
+
+require_once(PATH::ENTITY . "Entity.php");
+require_once(PATH::ENTITY . "Lang.php");
+
+/**
+ * CV (resume).
+ *
+ * Represents an object from the table 'cv'.
+ */
+class Cv extends Entity{
+
+    /**
+     * CV file identifier.
+     */
+    private $id;
+
+    /**
+     * CV {@see Lang}.
+     */
+    private $lang;
+
+    /**
+     * Filename.
+     */
+    private $file;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * object, populating it.
+     *
+     * @param string $id CV id.
+     */
+    public function __construct($id){
+        $statement = get_context()->get_db()->prepare("SELECT lang, file FROM cv WHERE id = :id");
+        $statement->bindValue(':id', $id, PDO::PARAM_INT);
+        $statement->execute();
+        $r_cv = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_cv !== false){
+            $this->id = $id;
+            $this->lang = new Lang($r_cv["lang"]);
+            $this->file = $r_cv["file"];
+        }
+    }
+
+    /**
+     * Retrieves the CV identifier.
+     *
+     * @return int CV ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+
+    /**
+     * Retrieves the CV language.
+     *
+     * @return int CV language.
+     */
+    public function get_lang(){
+        return $this->lang;
+    }
+
+    /**
+     * Retrieves the path to the CV file.
+     *
+     * @return int CV file path.
+     */
+    public function get_file(){
+        return $this->file;
+    }
+}
+?>

+ 18 - 0
application/entity/Entity.php

@@ -0,0 +1,18 @@
+<?php
+/**
+ * Entity superclass file.
+ *
+ * Provides a class to extend all entities from.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IVV
+ */
+
+/**
+ * Entity superclass.
+ *
+ * Every other entity must inherit from this one. It represents a database
+ * entity.
+ */
+abstract class Entity{}

+ 77 - 0
application/entity/Lang.php

@@ -0,0 +1,77 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+
+
+/**
+ * Language.
+ *
+ * Represents an object from the table 'lang'.
+ */
+class Lang extends Entity{
+
+    /**
+     * Lowercase, two-letter language code.
+     */
+    private $code;
+
+    /**
+     * Language name, in it's own language.
+     */
+    private $name;
+
+    /**
+     * Indictes wether the language is offered in this language.
+     */
+    private $active;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * object, populating it.
+     *
+     * @param int $id Database identifier of the language.
+     */
+    public function __construct($id){
+        $statement = get_context()->get_db()->prepare(
+          "SELECT code, name, active FROM lang WHERE code = :id"
+        );
+        $statement->bindValue(':id', $id, PDO::PARAM_STR);
+        $statement->execute();
+        $r_lang = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_lang !== false){
+            $this->code = $r_lang["code"];
+            $this->name = $r_lang["name"];
+            $this->active = $r_lang["active"];
+        }
+    }
+
+    /**
+     * Retrieves the language identifier code.
+     *
+     * @return int Language code.
+     */
+    public function get_code(){
+        return $this->code;
+    }
+
+    /**
+     * Retrieves the language name.
+     *
+     * @return int Language name.
+     */
+    public function get_name(){
+        return $this->name;
+    }
+
+    /**
+     * Checks if the language is active.
+     *
+     * @return bool True if the language is active, false otherwise.
+     */
+    public function is_active(){
+        return $this->active;
+    }
+}
+?>

+ 113 - 0
application/entity/License.php

@@ -0,0 +1,113 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+
+
+/**
+ * License.
+ *
+ * Represents an object from the table 'license'.
+ */
+class License extends Entity{
+
+    /**
+     * @var string License identifier. Usually an abbreviation.
+     */
+    private $id;
+
+    /**
+     * @var string Short, easily readable text summarizing the full text of
+     * the license.
+     */
+    private $summary;
+
+    /**
+     * @var string Full text of the license.
+     */
+    private $legal;
+
+    /**
+     * @var string License logo.
+     */
+    private $logo;
+
+    /**
+     * @var string License icon, small.
+     */
+    private $icon;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * object, populating it.
+     *
+     * @param string $id Identifier of the license.
+     */
+    public function __construct($id){
+        $statement = get_context()->get_db()->prepare("
+          SELECT id, summary, legal, logo, icon
+          FROM license
+          WHERE id = :id
+        ");
+        $statement->bindValue(':id', $id, PDO::PARAM_STR);
+        $statement->execute();
+        $r_license = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_license !== false){
+            $this->id = $r_license["id"];
+            $this->summary = TEXT::get($r_license["summary"]);
+            $this->legal = TEXT::get($r_license["legal"]);
+            $this->logo = $r_license["logo"];
+            $this->icon = $r_license["icon"];
+        }
+        else{
+            Log::warn("License with id '$id' doesn't exist");
+        }
+    }
+    
+    /**
+     * Retrieves the license identifier
+     *
+     * @return string License ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+    
+    /**
+     * Retrieves a short, easily readable text summarizing the full text of
+     * the license.
+     *
+     * @return string License summary.
+     */
+    public function get_summary(){
+        return $this->summary;
+    }
+    
+    /**
+     * Retrieves the full text of the license
+     *
+     * @return string License legal text.
+     */
+    public function get_legal(){
+        return $this->legal;
+    }
+    
+    /**
+     * Retrieves the license logo.
+     *
+     * @return string The logo filename.
+     */
+    public function get_logo(){
+        return $this->logo;
+    }
+    
+    /**
+     * Retrieves the license icon.
+     *
+     * @return string The icon filename.
+     */
+    public function get_icon(){
+        return $this->icon;
+    }
+}

+ 416 - 0
application/entity/Project.php

@@ -0,0 +1,416 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+require_once(PATH::ENTITY . "License.php");
+require_once(PATH::ENTITY . "Project_Type.php");
+require_once(PATH::ENTITY . "Project_Image.php");
+require_once(PATH::ENTITY . "Project_Url.php");
+
+
+/**
+ * Project.
+ *
+ * Represents an object from the table 'project'.
+ */
+class Project extends Entity{
+
+    /**
+     * @var int Project identifier.
+     */
+    private $id;
+
+    /**
+     * @var string Permalink for linking the project (relative).
+     */
+    private $permalink;
+
+    /**
+     * @var int Project index for sorting.
+     */
+    private $index;
+
+    /**
+     * @var int Project type identifier.
+     */
+    private $type_id;
+    
+    /**
+     * @var Project_Type Type of project.
+     */
+    private $type;
+
+    /**
+     * @var string Project title in the selected language.
+     */
+    private $title;
+
+    /**
+     * @var string Project logo filename.
+     */
+    private $logo;
+
+    /**
+     * @var string Project summary in the selected language.
+     */
+    private $header;
+
+    /**
+     * @var String Project description in the selected language.
+     */
+    private $text;
+
+    /**
+     * @var String Project commentary by the developer.
+     */
+    private $comment;
+    
+    /**
+     * @var int License identifier.
+     */
+    private $license_id;
+
+    /**
+     * @var License Project license.
+     */
+    private $license;
+
+    /**
+     * @var Project_Image[] List of project images.
+     */
+    private $images = [];
+
+    /**
+     * @var Project_Tag[] List of project tags.
+     */
+    private $tags = [];
+
+    /**
+     * @var Project_URL[] List of the project URLs
+     */
+    private $urls = [];
+    
+    /**
+     * @var bool[] Control flags to check loading status.
+     */
+    private $load_flags = [
+        "type" => false,
+        "images" => false,
+        "tags" => false,
+        "urls" => false,
+        "license" => false
+    ];
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * project, populating it and it's items.
+     *
+     * @param string $id Identifier or permalink of the project.
+     */
+    public function __construct($id){
+        Log::debug("Loading project with ID: " . $id);
+        $statement = get_context()->get_db()->prepare("
+          SELECT id, permalink, idx, type, title, logo, header, text, comment, license
+          FROM project
+          WHERE id = :id OR permalink = :permalink
+          LIMIT 1
+        ");
+        $statement->bindValue(':id', $id, PDO::PARAM_INT);
+        $statement->bindValue(':permalink', $id, PDO::PARAM_STR);
+        $statement->execute();
+        $r_project = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_project !== false){
+            $this->id = $r_project["id"];
+            $this->permalink = $r_project["permalink"];
+            $this->idx = $r_project["idx"];
+            $this->title = Text::get($r_project["title"]);
+            $this->logo = $r_project["logo"];
+            $this->header = Text::get($r_project["header"], false);
+            $this->text = Text::get($r_project["text"], false);
+            $this->comment = Text::get($r_project["comment"], false);
+            $this->type_id = $r_project["type"];
+            $this->license_id = $r_project["license"];
+        }
+        else{
+            Log::debug("Project with id '$id' doesn't exist");
+        }
+    }
+
+    /**
+     * Retrieves the project identifier
+     *
+     * @return int Project ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+
+    /**
+     * Retrieves the project permalink
+     *
+     * @return string Project permalink
+     */
+    public function get_permalink(){
+        return $this->permalink;
+    }
+
+    /**
+     * Retrieves the project index for sorting purpuses.
+     *
+     * @return int Project index
+     */
+    public function get_index(){
+        return $this->index;
+    }
+
+    /**
+     * Loads the project type information.
+     * 
+     * Not exposed, must be called before accessing the project type.
+     */
+    private function load_type(){
+        if ($this->load_flags["type"] === false){
+            $this->type = new Project_Type($this->type_id);
+            $this->load_flags["type"] = true;
+        }
+    }
+    
+    /**
+     * Retrieves the project type.
+     *
+     * @return Project_Type The project type.
+     */
+    public function get_type(){
+        if ($this->load_flags["type"] === false){
+            $this->load_type();
+        }
+        return $this->type;
+    }
+
+    /**
+     * Retrieves the project title.
+     *
+     * @return string Title.
+     */
+    public function get_title(){
+        return $this->title;
+    }
+
+    /**
+     * Retrieves the project logo.
+     *
+     * @return string The logo filename.
+     */
+    public function get_logo(){
+        return $this->logo;
+    }
+
+    /**
+     * Retrieves a short project description.
+     *
+     * @return string Project header.
+     */
+    public function get_header(){
+        return $this->header;
+    }
+
+    /**
+     * Retrieves the project description.
+     *
+     * @return string Project description text.
+     */
+    public function get_text(){
+        return $this->text;
+    }
+
+    /**
+     * Retrieves the project comment.
+     *
+     * @return string Project comment text.
+     */
+    public function get_comment(){
+        return $this->comment;
+    }
+
+    /**
+     * Loads the project license information.
+     *
+     * Not exposed, must be called before accessing the project license.
+     */
+    private function load_license(){
+        if ($this->load_flags["license"] === false){
+            $this->license = new License($this->license_id);
+            $this->load_flags["license"] = true;
+        }
+    }
+
+    /**
+     * Retrieves the project license.
+     *
+     * @return License Project license.
+     */
+    public function get_license(){
+        if ($this->load_flags["license"] === false){
+            $this->load_license();
+        }
+        return $this->license;
+    }
+    
+    /**
+     * Loads the project images.
+     *
+     * Not exposed, must be called before accessing the project images.
+     */
+    private function load_images(){
+        if ($this->load_flags["images"] === false){
+            $statement = get_context()->get_db()->prepare("
+              SELECT id 
+              FROM project_image
+              WHERE project = :id
+              ORDER BY idx
+            ");
+            $statement->bindValue(':id', $this->id, PDO::PARAM_INT);
+            $statement->execute();
+            while ($r_image = $statement->fetch(PDO::FETCH_ASSOC)){
+                array_push($this->images, new Project_Image($r_image["id"]));
+            }
+            $this->load_flags["images"] = true;
+        }
+    }
+
+    /**
+     * Retrieves the projcet images
+     *
+     * @return Project_Image[] List of the project images.
+     */
+    public function get_images(){
+        if ($this->load_flags["images"] === false){
+            $this->load_images();
+        }
+        return $this->images;
+    }
+    
+    /**
+     * Loads the project tags.
+     *
+     * Not exposed, must be called before accessing the project tags.
+     */
+    private function load_tags(){
+        if ($this->load_flags["tags"] === false){
+            $statement = get_context()->get_db()->prepare("
+              SELECT tag
+              FROM project_tag
+              WHERE project = :id
+            ");
+            $statement->bindValue(':id', $this->id, PDO::PARAM_INT);
+            $statement->execute();
+            while ($r_tag = $statement->fetch(PDO::FETCH_ASSOC)){
+                array_push($this->tags, Text::get($r_tag["tag"]));
+            }
+            $this->load_flags["tags"] = true;
+        }
+    }
+
+    /**
+     * Retrieves the project tags.
+     *
+     * @return Project_Tag[] Project tags. 
+     */
+    public function get_tags(){
+        if ($this->load_flags["tags"] === false){
+            $this->load_tags();
+        }
+        return $this->tags;
+    }
+    
+    /**
+     * Loads the project URLs.
+     *
+     * Not exposed, must be called before accessing the project URLs.
+     */
+    private function load_urls(){
+        if ($this->load_flags["urls"] === false){
+            $statement = get_context()->get_db()->prepare("
+              SELECT id
+              FROM project_url
+              WHERE project = :id
+            ");
+            $statement->bindValue(':id', $this->id, PDO::PARAM_INT);
+            $statement->execute();
+            while ($r_url = $statement->fetch(PDO::FETCH_ASSOC)){
+                array_push($this->urls, new Project_Url($r_url["id"]));
+            }
+            $this->load_flags["urls"] = true;
+        }
+    }
+
+    /**
+     * Retrieves the project URLs
+     *
+     * @return Project_URL[] List of the project URLs. 
+     */
+    public function get_urls(){
+        if ($this->load_flags["urls"] === false){
+            $this->load_urls();
+        }
+        return $this->urls;
+    }
+
+    /**
+     * Retrieves a state flag
+     *
+     * @return boolean Flag value, false if it doesn't exist. 
+     */
+    private function get_flag($flag){
+        $value = false;
+        if (array_key_exists($flag , $this->load_flags)){
+            $value = $this->load_flags[$flag];
+        }
+        return $value;
+    }
+
+    /**
+     * Sets a load status flag.
+     * 
+     * If, once set, all flags are set to true, mark_as_complete(true)
+     * will be automatically called.
+     * 
+     * @param string $flag Flag name.
+     * @param boolean $value Flag name.
+     */
+    private function set_flag($flag, $value){
+        $key_found = false;
+        $keys = array_keys($this->load_flags);
+        foreach($keys as $key){
+            if ($key === $flag){
+                $key_found = true;
+                break;
+            }
+        }
+        if ($key_found == false){
+            Log::warn("Trying to set non-existing flag '$flag' in project");
+            return false;
+        }
+        else{
+            if ($value !== true && $value !== false){
+                Log::warn("Trying to set project flag '$flag' to an invalid value: " . $value);
+                return false;
+            }
+            else{
+                $this->load_flags[$flag] = $value;
+                
+                // Once set, loop all keys to check if they are all true
+                $all_loaded = true;
+                foreach($this->load_flags as $value){
+                    if ($value !== true){
+                        $all_loaded = false;
+                        break;
+                    }
+                }
+                $this->mark_as_complete($all_loaded);
+                return true;
+            }
+        }
+    }
+}

+ 109 - 0
application/entity/Project_Image.php

@@ -0,0 +1,109 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+
+
+/**
+ * Image of a project.
+ *
+ * Represents an object from the table 'project_image'.
+ */
+class Project_Image extends Entity{
+
+    /**
+     * @var int Image identifier.
+     */
+    private $id;
+
+    /**
+     * @var int Project identifier.
+     */
+    private $project;
+
+    /**
+     * @var int Indicates the order position among other images.
+     */
+    private $idx;
+
+    /**
+     * @var string Filename of the image.
+     */
+    private $image;
+
+    /**
+     * @var string Alternative text for the image.
+     */
+    private $alt;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * image, populating it.
+     *
+     * @param int $id Identifier of the image.
+     */
+    public function __construct($id){
+        $statement = get_context()->get_db()->prepare("
+          SELECT id, project, idx, image, alt FROM project_image WHERE id = :id
+        ");
+        $statement->bindValue(':id', $id, PDO::PARAM_INT);
+        $statement->execute();
+        $r_image = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_image !== false){
+            $this->id = $r_image["id"];
+            $this->project = $r_image["project"];
+            $this->idx = $r_image["idx"];
+            $this->image = $r_image["image"];
+            $this->alt = Text::get($r_image["alt"]);
+        }
+        else{
+            Log::warn("Project image with id '$id' doesn't exist");
+        }
+    }
+    
+    /**
+     * Retrieves the image identifier
+     *
+     * @return int Image ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+    
+    /**
+     * Retrieves the project identifier
+     *
+     * @return int Project ID.
+     */
+    public function get_project(){
+        return $this->project;
+    }
+    
+    /**
+     * Retrieves the image index for sorting.
+     *
+     * @return int Image index.
+     */
+    public function get_idx(){
+        return $this->idx;
+    }
+    
+    /**
+     * Retrieves the image filename.
+     *
+     * @return string Image filename.
+     */
+    public function get_image(){
+        return $this->image;
+    }
+
+    /**
+     * Retrieves the image alternative text or title.
+     *
+     * @return string Image text.
+     */
+    public function get_text(){
+        return $this->alt;
+    }
+}

+ 37 - 0
application/entity/Project_Tag.php

@@ -0,0 +1,37 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+
+
+/**
+ * Tag of a project.
+ *
+ * Represents an object from the table 'project_tag'.
+ */
+class Project_Tag extends Entity{
+
+    /**
+     * Identifier of the project the tag belongs to.
+     */
+    public $project;
+
+    /**
+     * Tag text.
+     */
+    public $tag;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * tag, populating it.
+     *
+     * @param int $project Identifier of the project the tag belongs to.
+     * @param string $tag Tag identifier. Same as it's text.
+     */
+    public function __construct($db, $lang, $project, $tag){
+        parent::__construct($db, $lang);
+        $this->project = $project;
+        $this->tag = text($this, $tag);
+    }
+}

+ 86 - 0
application/entity/Project_Type.php

@@ -0,0 +1,86 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+
+
+/**
+ * Type of a project.
+ *
+ * Represents an object from the table 'project_type'.
+ */
+class Project_Type extends Entity{
+
+    /**
+     * @var int Identifier of the project type.
+     */
+    private $id;
+
+    /**
+     * @var String Type denomination, in the defined language.
+     */
+    private $title;
+
+    /**
+     * @var String Type description, in the defined language.
+     */
+    private $summary;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * type, populating it.
+     *
+     * @param string $id Identifier of the project type.
+     */
+    public function __construct($id){
+        $statement = get_context()->get_db()->prepare("
+          SELECT
+            id,
+            title,
+            summary
+          FROM project_type
+          WHERE id = :id;
+        ");
+        $statement->bindValue(':id', $id, PDO::PARAM_INT);
+        $statement->execute();
+        $r_type = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_type !== false){
+            $this->id = $r_type["id"];
+            $this->title = TEXT::get($r_type["title"]);
+            $this->summary = TEXT::get($$r_type["summary"]);
+            $this->mark_as_loaded(true);
+            $this->mark_as_complete(true);
+        }
+        else{
+            Log::warn("Project type with id '$id' doesn't exist");
+        }
+    }
+    
+    /**
+     * Retrieves the project type identifier
+     *
+     * @return int Project type ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+    
+    /**
+     * Retrieves the project type title.
+     *
+     * @return string Type name.
+     */
+    public function get_title(){
+        return $this->title;
+    }
+    
+    /**
+     * Retrieves the project type description.
+     *
+     * @return string Type summary.
+     */
+    public function get_summary(){
+        return $this->summary;
+    }
+}

+ 97 - 0
application/entity/Project_Url.php

@@ -0,0 +1,97 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+require_once(PATH::ENTITY . "Project_Url_Type.php");
+
+
+/**
+ * URL related to a project.
+ *
+ * Represents an object from the table 'project_url'.
+ */
+class Project_Url extends Entity{
+
+    /**
+     * @var int Identifier of the URL.
+     */
+    private $id;
+
+    /**
+     * @var int Identifier of the project the URL is related to.
+     */
+    private $project;
+
+    /**
+     * @var Project_Url_Type URL type.
+     */
+    private $type;
+
+    /**
+     * @var string Full URL address.
+     */
+    private $url;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * url, populating it and its items.
+     *
+     * @param int $id Identifier of the URL.
+     */
+    public function __construct($id){
+        $statement = get_context()->get_db()->prepare("
+          SELECT id, project, type, url
+          FROM project_url
+          WHERE id = :id
+        ");
+        $statement->bindValue(':id', $id, PDO::PARAM_INT);
+        $statement->execute();
+        $r_url = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_url !== false){
+            $this->id = $r_url["id"];
+            $this->project = $r_url["project"];
+            $this->type = new Project_Url_Type($r_url["type"]);
+            $this->url = $r_url["url"];
+        }
+        else{
+            Log::warn("Project URL with id '$id' doesn't exist");
+        }
+    }
+    
+    /**
+     * Retrieves the URL identifier
+     *
+     * @return int URL ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+    
+    /**
+     * Retrieves the project identifier
+     *
+     * @return int Project ID.
+     */
+    public function get_project(){
+        return $this->project;
+    }
+    
+    /**
+     * Retrieves the url type.
+     *
+     * @return Project_Url_Type URL type.
+     */
+    public function get_type(){
+        return $this->type;
+    }
+    
+    /**
+     * Retrieves the URL
+     *
+     * @return string The URL.
+     */
+    public function get_url(){
+        return $this->url;
+    }
+}

+ 96 - 0
application/entity/Project_Url_Type.php

@@ -0,0 +1,96 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+
+
+/**
+ * Type of URL.
+ *
+ * Represents an object from the table 'project_url_type'.
+ */
+class Project_Url_Type extends Entity{
+
+    /**
+     * URL type identifier.
+     */
+    private $id;
+
+    /**
+     * URL denomination, in the defined language.
+     */
+    private $title;
+
+    /**
+     * URL short explanation, in the defined language.
+     */
+    private $summary;
+
+    /**
+     * Filename of the logo of the URL. Usually, the logo of the site it
+     * points to.
+     */
+    private $logo;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the
+     * url type, populating it and its items.
+     *
+     * @param MySQL_connection $db Connection to the database.
+     * @param string $lang Lowercase, two-letter language code.
+     * @param int $id Identifier of the url type.
+     */
+    public function __construct($id){
+        $statement = get_context()->get_db()->prepare("
+          SELECT id, title, summary, logo
+          FROM project_url_type
+          WHERE id = :id
+        ");
+        $statement->bindValue(':id', $id, PDO::PARAM_STR);
+        $statement->execute();
+        $r_type = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_type != false){
+            $this->id = $r_type["id"];
+            $this->title = Text::get($r_type["title"]);
+            $this->summary = Text::get($r_type["summary"]);
+            $this->logo = $r_type["logo"];
+        }
+    }
+
+    /**
+     * Retrieves the project url typeidentifier
+     *
+     * @return int Project url type ID.
+     */
+    public function get_id(){
+        return $this->id;
+    }
+
+    /**
+     * Retrieves the project url type title.
+     *
+     * @return string Title.
+     */
+    public function get_title(){
+        return $this->title;
+    }
+
+    /**
+     * Retrieves the project url type summary.
+     *
+     * @return string The logo filename.
+     */
+    public function get_summary(){
+        return $this->summary;
+    }
+
+    /**
+     * Retrieves the project url type logo.
+     *
+     * @return string The logo filename.
+     */
+    public function get_logo(){
+        return $this->logo;
+    }
+}

+ 85 - 0
application/entity/Social.php

@@ -0,0 +1,85 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+
+/**
+ * Social.
+ *
+ * Represents a social link.
+ */
+class Social extends Entity{
+
+    /**
+     * @var int User profile URL.
+     */
+    private $url;
+
+    /**
+     * @var string Social site name.
+     */
+    private $site_name;
+
+    /**
+     * @var string Path to the site logo.
+     */
+    private $logo;
+
+    /**
+     * @var string Link text.
+     */
+    private $text;
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the social link, populating it.
+     */
+    public function __construct($id){
+        $user_id = parse_ini_file(__DIR__ . "/../config/config.ini", true)["user"]["id"];
+        $statement = get_context()->get_db()->prepare("
+          SELECT site_name, url, logo, text
+          FROM social, user_social
+          WHERE social.id = user_social.social AND user = :user AND user_social.id = :id
+        ");
+        $statement->bindValue(':id', $id, PDO::PARAM_INT);
+        $statement->bindValue(':user', $user_id, PDO::PARAM_INT);
+        $statement->execute();
+        $r_social = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_social !== false){
+            $this->url = $r_social["url"];
+            $this->site_name = $r_social["site_name"];
+            $this->logo = $r_social["logo"];
+            $this->text = Text::get($r_social["text"]);
+        }
+        else Log::error("Social link with id '$id' doesn't exist for user '$user_id'");
+    }
+
+    /**
+     * Retrieves the link URL.
+     *
+     * @return string link URL.
+     */
+    public function get_url(){return $this->url;}
+
+    /**
+     * Retrieves the social site name.
+     *
+     * @return string The social site name.
+     */
+    public function get_site_name(){return $this->site_name;}
+
+    /**
+     * Retrieves the site logo.
+     *
+     * @return string The site logo file path.
+     */
+    public function get_logo(){return $this->logo;}
+
+    /**
+     * Retrieves the social link text.
+     *
+     * @return string The social link text.
+     */
+    public function get_text(){return $this->text;}
+
+}

+ 231 - 0
application/entity/User.php

@@ -0,0 +1,231 @@
+<?php
+
+require_once(PATH::ENTITY . "Entity.php");
+require_once(PATH::ENTITY . "Lang.php");
+require_once(PATH::ENTITY . "Social.php");
+
+/**
+ * User.
+ *
+ * Represents an object from the table 'user'.
+ */
+class User extends Entity{
+
+    /**
+     * @var int User identifier.
+     */
+    private $id;
+
+    /**
+     * @var string User's display name.
+     */
+    private $display_name;
+
+
+    /**
+     * @var string User's image path.
+     */
+    private $image;
+
+    /**
+     * @var Lang[] List of the user languages.
+     */
+    private $langs = [];
+
+    /**
+     * @var Email[] List of the user emails.
+     */
+    private $email = [];
+
+    /**
+     * @var Social[] List of the user social links.
+     */
+    private $social = [];
+
+    /**
+     * @var string[] List uf user-defined texts.
+     */
+    private $texts = [];
+
+    /**
+     * @var bool[] Control flags to check loading status.
+     */
+    private $load_flags = [
+        "email" => false,
+        "social" => false,
+        "lang" => false
+    ];
+
+    /**
+     * Constructor.
+     *
+     * Searches the database and retrieves the information about the user, populating it. The
+     * user to be loaded must be defined in the configuration file.
+     */
+    public function __construct(){
+        $this->id = parse_ini_file(__DIR__ . "/../config/config.ini", true)["user"]["id"];
+        $statement = get_context()->get_db()->prepare(
+          "SELECT id, name, image FROM user WHERE id = :id"
+        );
+        $statement->bindValue(':id', $this->id, PDO::PARAM_INT);
+        $statement->execute();
+        $r_user = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r_user !== false){
+            $this->id = $r_user["id"];
+            $this->display_name = $r_user["name"];
+            $this->image = $r_user["image"];
+        }
+        else Log::error("User with id '$this->id' doesn't exist");
+    }
+
+    /**
+     * Retrieves the user identifier
+     *
+     * @return int User ID.
+     */
+    public function get_id(){return $this->id;}
+
+    /**
+     * Retrieves the user's display name.
+     *
+     * @return string The user's display name.
+     */
+    public function get_display_name(){return $this->display_name;}
+
+    /**
+     * Retrieves a user defined text text.
+     *
+     * @param string $key Text key.
+     * @return string The defined text, or an empty string if it doesn't exist.
+     */
+    public function get_text($key){
+        if (isset($this->texts[$key])) return $this->texts[$key];
+        $statement = get_context()->get_db()->prepare(
+          "SELECT text FROM user_text WHERE user = :user AND upper(name) = upper(:key)"
+        );
+        $statement->bindValue(':user', $this->id, PDO::PARAM_INT);
+        $statement->bindValue(':key', $key, PDO::PARAM_STR);
+        $statement->execute();
+        $r = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($r == false) return "";
+        $this->texts[$key] = Text::get($r["text"], false);
+        return $this->texts[$key];
+    }
+
+    /**
+     * Retrieves the user's profile image file path.
+     *
+     * @return string The user's profile image.
+     */
+    public function get_image(){return $this->image;}
+
+    /**
+     * Retrieves the user's list of emails.
+     *
+     * @return string[] The user's list of emails.
+     */
+    public function get_emails(){
+        if ($this->load_flags["email"] === false) $this->load_email();
+        return $this->email;
+    }
+
+    /**
+     * Retrieves the user's first visible email address.
+     *
+     * @return string The user's first visible email, null if there are none.
+     */
+    public function get_first_email(){
+        if ($this->load_flags["email"] === false) $this->load_email();
+        if (sizeof($this->email) == 0) return null;
+        else return $this->email[0];
+    }
+
+    /**
+     * Retrieves the user's list of supported languages.
+     *
+     * @return string[] The user's list of languages, sorted by priority.
+     */
+    public function get_langs(){
+        if ($this->load_flags["lang"] === false) $this->load_lang();
+        return $this->langs;
+    }
+
+    /**
+     * Retrieves the user's list of social links.
+     *
+     * @return Social[] The user's list of social links.
+     */
+    public function get_social(){
+        if ($this->load_flags["social"] === false) $this->load_social();
+        return $this->social;
+    }
+    
+    /**
+     * Checks if a language is supported by the user.
+     * 
+     * @param sting $code Two letter language code to check, case insensitive.
+     * @return True if the language is supported by the user, false otherwise.
+     */
+    public function is_valid_language($code){
+        if ($this->load_flags["lang"] === false) $this->load_lang();
+        foreach ($this->langs as $lang){
+            if (strtolower($lang->get_code()) == strtolower($code))
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Loads the user emails.
+     *
+     * Not exposed, must be called before accessing the user email list.
+     */
+    private function load_email(){
+        if ($this->load_flags["email"] === false){
+            $statement = get_context()->get_db()->prepare(
+              "SELECT email FROM user_email WHERE user = :id AND visible = 1"
+            );
+            $statement->bindValue(':id', $this->id, PDO::PARAM_INT);
+            $statement->execute();
+            while ($r_email = $statement->fetch(PDO::FETCH_ASSOC))
+                array_push($this->email, $r_email["email"]);
+            $this->load_flags["email"] = true;
+        }
+    }
+
+    /**
+     * Loads the user supported languages.
+     *
+     * Not exposed, must be called before accessing the user languages.
+     */
+    private function load_lang(){
+        if ($this->load_flags["lang"] === false){
+            $statement = get_context()->get_db()->prepare(
+              "SELECT lang FROM user_lang WHERE user = :id ORDER BY priority"
+            );
+            $statement->bindValue(':id', $this->id, PDO::PARAM_INT);
+            $statement->execute();
+            while ($r_lang = $statement->fetch(PDO::FETCH_ASSOC))
+                array_push($this->langs, new Lang($r_lang["lang"]));
+            $this->load_flags["lang"] = true;
+        }
+    }
+
+    /**
+     * Loads the user social links.
+     *
+     * Not exposed, must be called before accessing the user links.
+     */
+    private function load_social(){
+        if ($this->load_flags["social"] === false){
+            $statement = get_context()->get_db()->prepare(
+              "SELECT id FROM user_social WHERE user = :id ORDER BY priority"
+            );
+            $statement->bindValue(':id', $this->id, PDO::PARAM_INT);
+            $statement->execute();
+            while ($r_social = $statement->fetch(PDO::FETCH_ASSOC))
+                array_push($this->social, new Social($r_social["id"]));
+            $this->load_flags["social"] = true;
+        }
+    }
+}

+ 126 - 0
application/helper/DB_Helper.php

@@ -0,0 +1,126 @@
+<?php
+/**
+ * DB helper file.
+ *
+ * Provides a helper to perform database operations.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+require_once(__DIR__ . "/Helper.php");
+
+/**
+ * Database helper.
+ *
+ * Contains database utilities to connect.
+ *
+ * @category Helper.
+ */
+final class DB extends Helper{
+
+    private static function generate_dsn($db_configuration){
+        
+        $type = $db_configuration["type"];
+        $host = $db_configuration["host"];
+        $port = $db_configuration["port"];
+        $name = $db_configuration["name"];
+        Log::debug("Database type: $type");
+        Log::debug("Database host: $host");
+        Log::debug("Database port: $port");
+        Log::debug("Database name: $name");
+        $dsn = "";
+        switch ($type){
+            case "mysql":
+                $dsn = "mysql:";
+                if (filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) == false){
+                    Log::fatal(
+                      "MySQL driver specified, but hostname is invalid: $host",
+                      EXCEPTION_CODE::PDO_MYSQL_HOSTNAME
+                      );
+                    throw new UnexpectedValueException(
+                      "MySQL driver specified, but hostname is invalid: $host",
+                      EXCEPTION_CODE::PDO_MYSQL_HOSTNAME
+                    );
+                }
+                else{
+                    $dsn .= "host=$host;";
+                }
+                if (
+                  strlen($name) > 64 ||
+                  strpos($name, "\\") !== false ||
+                  strpos($name, "/") !== false ||
+                  strpos($name, ".") !== false
+                ){
+                    Log::fatal(
+                      "MySQL driver specified, but database name is invalid: $name",
+                      EXCEPTION_CODE::PDO_MYSQL_DBNAME
+                    );
+                    throw new UnexpectedValueException(
+                      "MySQL driver specified, but database name is invalid: $name",
+                      EXCEPTION_CODE::PDO_MYSQL_DBNAME
+                    );
+                }
+                else{
+                    $dsn .= "dbname=$name;";
+                }
+                if (intval($port) != 0){
+                    $port = intval($port);
+                    if ($port < 1 || $port > 65534){
+                        Log::fatal(
+                          "MySQL driver specified, but port number name is invalid: $port",
+                          EXCEPTION_CODE::PDO_MYSQL_PORT
+                        );
+                        throw new UnexpectedValueException(
+                          "MySQL driver specified, but port number name is invalid: $port",
+                          EXCEPTION_CODE::PDO_MYSQL_PORT
+                        );
+                    }
+                    else{
+                        $dsn .= "port=$port;";
+                    }
+                }
+                
+                if ($host != null && strlen($host) > 0){
+                    $dsn .= "host=$host;";
+                }
+                break;
+            default:
+                Log::fatal(
+                  "Unsupported batabase driver '$type'.",
+                  EXCEPTION_CODE::PDO_UNSUPPORTED
+                );
+                throw new UnexpectedValueException(
+                  "Unsupported batabase driver '$type'.",
+                  EXCEPTION_CODE::PDO_UNSUPPORTED
+                );
+        }
+        $dsn .= "charset=UTF8;";
+        Log::debug("Database dsn: $dsn");
+        return $dsn;
+    }
+    
+    /**
+     * Creates a database connection using the data in the config files.
+     * 
+     * @return resource Connection to the database.
+     */
+    public static function start($db_configuration){
+        //ini_set('mssql.charset', 'UTF8');
+        $dsn = DB::generate_dsn($db_configuration);
+        $user = $db_configuration["user"];
+        $pass = $db_configuration["pass"];
+        
+        try{
+            $db = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => true));
+            Log::info("Successfully connected with database");
+            //$db->exec('SET NAMES utf8');
+            //$db->exec('SET CHARACTER SET utf8');
+            return $db;
+        }
+        catch(Exception $e){
+            Log::fatal("Connection failed" . $e->getMessage());
+        }
+    }
+}

+ 96 - 0
application/helper/HTML_Helper.php

@@ -0,0 +1,96 @@
+<?php
+/**
+ * HTML helper file.
+ *
+ * Provides utilities to generate HTML content.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+require_once(__DIR__ . "/Helper.php");
+
+
+/**
+ * HTML helper.
+ *
+ * Contains utilities to generate HTML content.
+ *
+ * @category Helper.
+ */
+final class HTML extends Helper{
+
+    /**
+     * This function closes all the opened HTML tags in a given string.
+     * 
+     * @param string html The string with HTML tags.
+     * @return string HTML with closed tags.
+     */
+    public static function close_tags($html) {
+        $result = [];
+        preg_match_all("#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU", $html, $result);
+        $openedtags = $result[1];
+        preg_match_all("#</([a-z]+)>#iU", $html, $result);
+        $closedtags = $result[1];
+        $len_opened = count($openedtags);
+        if (count($closedtags) == $len_opened) {
+            return $html;
+        }
+        $openedtags = array_reverse($openedtags);
+        for ($i=0; $i < $len_opened; $i++) {
+            if (!in_array($openedtags[$i], $closedtags)) {
+                $html .= "</".$openedtags[$i].">";
+            } else {
+                unset($closedtags[array_search($openedtags[$i], $closedtags)]);
+            }
+        }
+        return $html;
+    }
+
+
+    /**
+     * Text shortener. Given a string, it trims in the proximity of the
+     * desired string, up to the next white character. If indicated, it will
+     * append a link to the full text.
+     * 
+     * @param string $text The text to shorten.
+     * @param int $length The desired length.
+     * @param string $link_text Text for the link. Optional.
+     * @param string $link URI of the link.
+     * @return string Shortened text.
+     */
+     function cut_text($text, $length, $link_text = "", $link = ""){
+        if (strlen($text) < $length){
+            return $text;
+        }
+        $cut = substr($text, 0, strpos($text, " ", $length));
+        $cut = close_tags($cut);
+        if (strlen($cut) == 0){
+            $cut = $text;
+        }
+        if (strlen($text) != strlen($cut) && strlen($link) > 0 && strlen($link_text) > 0){
+            $cut = $cut . "... <a href='$link'>$link_text</a>";
+        }
+        return $cut;
+    }
+
+    /**
+     * Creates the srcset attribute for content images.
+     * Creates 9 different resolutions, from 100px to 900px.
+     * 
+     * @param string $file Path to the file, relative to $path["img"]["content"]).
+     * @return string srcset atttribute content.
+     */
+    public static function srcset($file){
+        $srcset = "";
+        $dir = dirname($file);
+        $name = basename($file);
+        foreach (range(1, 9) as $d) {
+            $srcset = $srcset . URL::IMG["CONTENT"] . $dir . "/x" . $d . "00/" . $name . " " . $d . "00px, ";
+        }
+        $srcset = rtrim($srcset, ", ");
+        return $srcset;
+    }
+
+}

+ 20 - 0
application/helper/Helper.php

@@ -0,0 +1,20 @@
+<?php
+/**
+ * Base helper file.
+ *
+ * Provides an empty helper.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+/**
+ * Helper superclass.
+ *
+ * Every other entity must inherit from this one. 
+ * 
+ * @abstract
+ * @category Helper
+ */
+abstract class Helper{}

+ 59 - 0
application/helper/NET_Helper.php

@@ -0,0 +1,59 @@
+<?php
+/**
+ * Netowrk helper file.
+ *
+ * Provides a helper to perform network operations.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+require_once(__DIR__ . "/Helper.php");
+
+/**
+ * Netowrk helper.
+ *
+ * Contains netowrk related utilities.
+ *
+ * @category Helper.
+ */
+final class NET extends Helper{
+    
+    /**
+     * Finds out the protocol the user is connecting to the site with.
+     *
+     * @return string "http://" or "https://".
+     */
+    public static function get_protocol(){
+        if (isset($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == "on" || $_SERVER["HTTPS"] == 1) || isset($_SERVER["HTTP_X_FORWARDED_PROTO"]) && $_SERVER["HTTP_X_FORWARDED_PROTO"] == "https") {
+            $protocol = "https://";
+        }
+        else {
+            $protocol = "http://";
+        }
+        return $protocol;
+    }
+    
+    /**
+     * Finds out the IP address of the client.
+     *
+     * @return string Client IP address.
+     */
+    public static function get_ip(){
+        $client  = @$_SERVER["HTTP_CLIENT_IP"];
+        $forward = @$_SERVER["HTTP_X_FORWARDED_FOR"];
+        $remote  = $_SERVER["REMOTE_ADDR"];
+        if(filter_var($client, FILTER_VALIDATE_IP)){
+            $ip = $client;
+        }
+        elseif(filter_var($forward, FILTER_VALIDATE_IP)){
+            $ip = $forward;
+        }
+        else{
+            $ip = $remote;
+        }
+        return $ip;
+    }
+    
+}

+ 266 - 0
application/helper/TEXT_Helper.php

@@ -0,0 +1,266 @@
+<?php
+/**
+ * DB helper file.
+ *
+ * Provides a helper to perform database operations.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+require_once(__DIR__ . "/Helper.php");
+
+
+/**
+ * Text helper.
+ *
+ * Contains utilities to show and manipulate texts.
+ *
+ * @category Helper.
+ */
+final class TEXT extends Helper{
+
+    /**
+     * Selects the language the page will be displayed on.
+     * 
+     * First, it tries to read the language cookie in the client. If none is set, get the browser
+     * language preference list. If none is provided or they are not supported, it will use the
+     * default language.
+     * 
+     * @return string Lowercase, two-letter language code.
+     * @todo Refactor to use user definced languages.
+     */
+    function select_language(){
+        // Get available languages from db
+        $available_languages = array();
+        $q_lang = mysqli_query($db, "SELECT code FROM lang WHERE active = 1;");
+        while ($r_lang = mysqli_fetch_array($q_lang))
+            array_push($available_languages, $r_lang["code"]);
+        // Is there a language cookie installed on the client?.
+        header("Cache-control: private");
+        if (isSet($_COOKIE["lang"])){
+            $lang = $_COOKIE["lang"];
+            if (in_array($lang, $available_languages)) return $lang;
+        }
+        // If no cookie, select from client browser preferences.
+        $lang = prefered_language($available_languages, $_SERVER["HTTP_ACCEPT_LANGUAGE"]);
+        if (in_array($lang, $available_languages)) return $lang;
+        // If no method was succesfull, default language
+        $lang = $available_languages[0];
+        return $lang;
+    }
+
+
+    /**
+     * Parses the language prefrences of the client broser, comparing them to the languages
+     * offered by the site and selects the prefered one among the ones supported.
+     * 
+     * @param string[] available_languages List of lowercase, two-letter language codes allowed by
+     * the site.
+     * @param string http_accept_language Raw header with info about client language preferences.
+     * @return string Lowercase, two-letter code of the prefered available language.
+     * @todo Refactor to user user defined languages.
+     */
+    function prefered_language(array $available_languages, $http_accept_language) {
+
+        $available_languages = array_flip($available_languages);
+        $langs = [];
+        preg_match_all(
+          "~([\w-]+)(?:[^,\d]+([\d.]+))?~", strtolower($http_accept_language),
+          $matches, PREG_SET_ORDER
+        );
+        foreach($matches as $match) {
+            list($a, $b) = explode("-", $match[1]) + array("", "");
+            $value = isset($match[2]) ? (float) $match[2] : 1.0;
+            if(isset($available_languages[$match[1]])) {
+                $langs[$match[1]] = $value;
+                continue;
+            }
+            if(isset($available_languages[$a])) $langs[$a] = $value - 0.1;
+        }
+        if (count($langs) > 0){
+            arsort($langs);
+            //return $langs[0];
+            return array_values($langs)[0];
+        }
+        else return 'en';
+    }
+
+
+    /**
+     * Returns a date string into a human readable string on the specified language. Only works
+     * for spanish, basque and english.
+     * 
+     * @param string $str_date Date string ('yyyy-mm-dd' or 'yyyy-mm-dd HH:MM:SS')
+     * @param string $lang Language code (es, en, eu).
+     * @param string $time Append the time at the end.
+     * @return string Formatted date string.
+     */
+    function format_date($str_date, $lang, $time = true){
+        $date = strtotime($str_date);
+        $year = date("o", $date);
+        $month = date("n", $date);
+        $month --;
+        $day = date("j", $date);
+        $wday = date("N", $date);
+        $wday --;
+        $hour = date("H", $date);
+        $minute = date("i", $date);
+        switch ($lang){
+            case "en":
+                $week = [
+                  "Monday", "Tuesday", "Wednesday", "Thursday",
+                  "Friday", "Saturday", "Sunday"
+                ];
+                $months = [
+                  "January", "February", "March", "April", "May", "June",
+                  "July", "August", "September", "October", "November", "December"
+                ];
+                if ($time) $str = "$week[$wday], $months[$month] $day, $year at $hour:$minute";
+                else $str = "$week[$wday], $months[$month] $day, $year";
+                break;
+            case "eu":
+                $week = [
+                  "astelehena", "asteartea", "asteazkena", "osteguna",
+                  "ostirala", "larumbata", "igandea"
+                ];
+                $months = [
+                  "urtarrilaren", "otsailaren", "martxoaren", "apirilaren",
+                  "maiatzaren","ekainaren", "uztailaren", "abuztuaren",
+                  "irailaren", "urriaren", "azaroaren", "abenduaren"
+                ];
+                if ($time)
+                    $str = $year . "ko $months[$month] $day" . "an, $week[$wday], $hour:$minute";
+                else $str = $year . "ko $months[$month] $day" . "an, $week[$wday]";
+                break;
+            default:
+                $week = [
+                  "Lunes", "Martes", "Mi&eacute;rcoles", "Jueves",
+                  "Viernes", "S&aacute;bado", "Domingo"
+                ];
+                $months = [
+                  "enero", " febrero", "marzo", "abril", "mayo", "junio",
+                  "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"
+                ];
+                if ($time)
+                    $str = "$week[$wday] $day de $months[$month] de $year a las $hour:$minute";
+                else $str = "$week[$wday] $day de $months[$month] de $year";
+        }
+        return $str;
+    }
+
+
+    /**
+     * Retrieves a text fragment from the database.
+     * 
+     * @param string $id Text identifier.
+     * @param bool $html True to decode for HTML.
+     * @returns string The text, or an empty string if it's not found.
+     */
+    public static function get($id, $html = true){
+        $statement = get_context()->get_db()->prepare(
+          "SELECT text, file FROM text WHERE id = :id AND lang = :lang"
+        );
+        $statement->bindValue(':id', $id, PDO::PARAM_STR);
+        $statement->bindValue(':lang', get_context()->get_lang(), PDO::PARAM_STR);
+        $statement->execute();
+        $row = $statement->fetch(PDO::FETCH_ASSOC);
+        if ($row !== false){
+            $text = "";
+            if (strlen($row["file"]) > 0) $text = file_get_contents(PATH::TEXT . $row["file"]);
+            else $text = $row["text"];
+            //$text = utf8_decode($text);
+            if ($html === true) $text = htmlentities($text, ENT_QUOTES | ENT_SUBSTITUTE);
+            return $text;
+        }
+        else{
+            Log::error("Text resource with id '$id' not found.");
+            return "";
+        }
+    }
+
+    /**
+     * Retrieves and formats  atext fragment from the database.
+     *
+     * @param string $id Text identifier.
+     * @param string[] $fragments Texts to replace each "#" character with.
+     * @param bool $html True to decode for HTML.
+     * @returns string The text.
+     */
+    public static function compose($id, $fragments, $html = true){
+        $text = Text::get($id, $html);
+        foreach ($fragments as $fragment){
+            $pos = strpos($text, "$");
+            if ($pos !== false)
+                $text = substr($text, 0, $pos) . $fragment . substr($text, $pos + 1);
+        }
+        return $text;
+    }
+
+
+    /**
+     * This function closes all the opened HTML tags in a given string.
+     * 
+     * @param string html The string with HTML tags.
+     * @return string HTML with closed tags.
+     */
+    function close_tags($html) {
+        $result = [];
+        preg_match_all(
+          "#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU", $html, $result
+        );
+        $openedtags = $result[1];
+        preg_match_all("#</([a-z]+)>#iU", $html, $result);
+        $closedtags = $result[1];
+        $len_opened = count($openedtags);
+        if (count($closedtags) == $len_opened) return $html;
+        $openedtags = array_reverse($openedtags);
+        for ($i=0; $i < $len_opened; $i++) {
+            if (!in_array($openedtags[$i], $closedtags)) $html .= "</".$openedtags[$i].">";
+            else unset($closedtags[array_search($openedtags[$i], $closedtags)]);
+        }
+        return $html;
+    }
+
+
+    /**
+     * Text shortener. Given a string, it trims in the proximity of the desired string, up to the
+     * next white character. If indicated, it will append a link to the full text.
+     * 
+     * @param string $text The text to shorten.
+     * @param int $length The desired length.
+     * @param string $link_text Text for the link. Optional.
+     * @param string $link URI of the link.
+     * @return string Shortened text.
+     */
+     function cut_text($text, $length, $link_text = "", $link = ""){
+        if (strlen($text) < $length) return $text;
+        $cut = substr($text, 0, strpos($text, " ", $length));
+        $cut = close_tags($cut);
+        if (strlen($cut) == 0) $cut = $text;
+        if (strlen($text) != strlen($cut) && strlen($link) > 0 && strlen($link_text) > 0)
+            $cut = $cut . "... <a href='$link'>$link_text</a>";
+        return $cut;
+    }
+
+    /**
+     * Creates the srcset attribute for content images.
+     *
+     * Creates 9 different resolutions, from 100px to 900px.
+     * 
+     * @param string $file Path to the file, relative to $path["img"]["content"]).
+     * @return string srcset atttribute content.
+     */
+    public static function srcset($file){
+        global $static;
+        $srcset = "";
+        $dir = dirname($file);
+        $name = basename($file);
+        foreach (range(1, 9) as $d)
+            $srcset .= $static["content"] . $dir . "/x" . $d . "00/" . $name . " " . $d . "00px, ";
+        $srcset = rtrim($srcset, ", ");
+        return $srcset;
+    }
+
+}

+ 44 - 0
application/helper/db.php

@@ -0,0 +1,44 @@
+<?php
+
+    /**
+     * Creates a database connection using the data in the config files.
+     * 
+     * @return MySQL_connection Connection to the database.
+     */
+    function start_db($db_configuration){
+        $type = $db_configuration["type"];
+        $host = $db_configuration["host"];
+        $port = $db_configuration["port"];
+        $name = $db_configuration["name"];
+        $user = $db_configuration["user"];
+        $pass = $db_configuration["pass"];
+        
+        $dbHost="localhost";
+        $dbName="myDB";
+        $dbUser="root";      //by default root is user name.
+        $dbPassword="";     //password is blank by default
+        try{
+            $dbConn= new PDO("mysql:host=$dbHost;dbname=$dbName",$dbUser,$dbPassword);
+            Echo "Successfully connected with myDB database";
+        } catch(Exception $e){
+            Echo "Connection failed" . $e->getMessage();
+        }  
+        
+        
+        // TODO: Types and port
+        $db = mysqli_connect($auth["host"], $auth["user"], $auth["pass"], $auth["name"]);
+
+        // Check connection
+        if (mysqli_connect_errno()){
+            error_log("Failed to connect to database: " . mysqli_connect_error());
+            return -1;
+        }
+
+        //Set encoding options
+        mysqli_set_charset($db, "utf-8");
+        mysqli_query($db, "SET NAMES utf8;");
+
+        return $db;
+    }
+
+?>

+ 45 - 0
application/page/Error_Page.php

@@ -0,0 +1,45 @@
+<?php
+
+    require_once(PATH::PAGE . "Page.php");
+
+    /**
+     * Error page model.
+     */
+    class Error_Page extends Page{
+
+        public $code = "";
+        public $error_description = "";
+        public $advice = "";
+        public $solution = [];
+
+        /**
+         * Constructor.
+         *
+         * Retrieves the data and initializes the page.
+         *
+         * @param MySQL_connection $db Connection to the database.
+         * @param string $lang Lowercase, two-letter language code.
+         */
+        public function __construct($db, $lang){
+            global $path;
+            global $base_url;
+            parent::__construct($db, $lang);
+            $this->view = PATH::VIEW . "error.php";
+            $this->set_view("error.php");
+            $this->code = http_response_code();
+            if (in_array($this->code, array(400, 401, 403, 404, 500))){
+                $err = $this->code;
+            }
+            else{
+                $err = "XXX";
+            }
+            $this->error_description = Text::get("ERROR_" . $err . "_DESCRIPTION");
+            $this->advice = Text::get("ERROR_" . $err . "_SOLUTION");
+            array_push($this->solution, Text::get("ERROR_" . $err . "_SOLUTION_0"));
+            array_push($this->solution, Text::get("ERROR_" . $err . "_SOLUTION_1"));
+            array_push($this->solution, Text::get("ERROR_" . $err . "_SOLUTION_2"));
+            $this->title = Text::get("ERROR_TITLE") . " " . $this->code . " - " . Text::get("USER_NAME");
+            $this->description = Text::get("ERROR_TITLE") . " " . $this->code . " - " . Text::get("USER_NAME");
+        }
+    }
+?>

+ 82 - 0
application/page/Help_Page.php

@@ -0,0 +1,82 @@
+<?php
+
+require_once(PATH::PAGE . "Page.php");
+
+/**
+ * Help page model.
+ */
+class Help_Page extends Page{
+
+    private $help = [
+        "info" => [
+            "title" => "",
+            "text" => "",
+        ],
+        "license" => [
+            "title" => "",
+            "text" => "",
+        ],
+        "privacy" => [
+            "title" => "",
+            "text" => "",
+        ],
+        "cookies" => [
+            "title" => "",
+            "text" => "",
+        ]
+    ];
+
+    /**
+     * Constructor.
+     *
+     * Retrieves the data and initializes the variables.
+     */
+    public function __construct(){
+        parent::__construct();
+        $this->set_view("help.php");
+        $this->help["info"]["title"] = Text::get("HELP_INFO_TITLE");
+        $this->help["info"]["text"] = get_context()->get_user()->get_text("HELP");
+        $this->help["license"]["title"] = Text::get("HELP_LICENSE_TITLE");
+        $this->help["license"]["text"] = get_context()->get_user()->get_text("LICENSE");
+        $this->help["privacy"]["title"] = Text::get("HELP_PRIVACY_TITLE");
+        $this->help["privacy"]["text"] = get_context()->get_user()->get_text("PRIVACY");
+        $this->help["cookies"]["title"] = Text::get("HELP_COOKIES_TITLE");
+        $this->help["cookies"]["text"] = get_context()->get_user()->get_text("COOKIES");
+        $this->set_title(Text::get("HELP_TITLE"));
+        $this->set_description(Text::get("HELP_DESCRIPTION") . " - " . Text::get("USER_TAGLINE"));
+        $this->set_canonical(URL::HELP);
+        $this->add_css("help.css");
+        $this->add_js("help.js");
+    }
+    
+    /**
+     * Retrives the text body for a section.
+     * 
+     * Available sections are "info", "license", "privacy" and "cookies".
+     * 
+     * @param string $section Section for which to retrieve the text, case insensitive.
+     * @return The text for the requested section, or an empty string if an invalid section is
+     * provided or the text is not defined.
+     */
+    public function get_section_text($section){
+        if (isSet($this->help[$section]))
+            if (isSet($this->help[$section]["text"])) return $this->help[$section]["text"];
+        return "";
+    }
+    
+    /**
+     * Retrives the title of a section.
+     * 
+     * Available sections are "info", "license", "privacy" and "cookies".
+     * 
+     * @param string $section Section for which to retrieve the title, case insensitive.
+     * @return The title for the requested section, or an empty string if an invalid section
+     * is provided or the title is not defined.
+     */
+    public function get_section_title($section){
+        if (isSet($this->help[$section]))
+            if (isSet($this->help[$section]["title"])) return $this->help[$section]["title"];
+        return "";
+    }
+}
+?>

+ 52 - 0
application/page/Home_Page.php

@@ -0,0 +1,52 @@
+<?php
+
+require_once(PATH::PAGE . "Page.php");
+require_once(PATH::ENTITY . "Project.php");
+
+/**
+ * Home page model.
+ */
+class Home_Page extends Page{
+
+    private $max_projects = 3;
+
+    /**
+     * List of recent projects.
+     */
+    private $projects = [];
+
+    /**
+     * Constructor.
+     *
+     * Retrieves the data and initializes the variables.
+     */
+    public function __construct(){
+        parent::__construct();
+        $this->set_view("home.php");
+        $this->set_title("");
+        $this->set_description(get_context()->get_user()->get_text("TAGLINE"));
+        $this->set_canonical("");
+        $this->add_css("home.css");
+        $this->set_code(200);
+        $this->set_message("OK");
+        $statement = get_context()->get_db()->prepare(
+          "SELECT id FROM project WHERE user = :user AND visible = 1 ORDER BY idx LIMIT :limit"
+        );
+        $statement->bindValue(':limit', $this->max_projects, PDO::PARAM_INT);
+        $statement->bindValue(':user', get_context()->get_user()->get_id(), PDO::PARAM_INT);
+        $statement->execute();
+        while ($r_project = $statement->fetch(PDO::FETCH_ASSOC)){
+            Log::debug("Instantiating new project: " . $r_project["id"]);
+            array_push($this->projects, new Project($r_project["id"]));
+        }
+        
+    }
+
+    /**
+     * Retrieves the projects to show on the page.
+     *
+     * @return Project[] Project list.
+     */
+    public function get_projects(){return $this->projects;}
+
+}

+ 320 - 0
application/page/Page.php

@@ -0,0 +1,320 @@
+<?php
+/**
+ * Page superclass file.
+ *
+ * Provides a class with all the properties and methods to display the page.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IVV
+ */
+
+/**
+ * Page superclass.
+ *
+ * Every visitable page type must inherit from this one.
+ *
+ * @category Page
+ */
+abstract class Page{
+
+    /**
+     * @var string[] List of CSS files required for the page.
+     */
+    private $css = [];
+    
+    /**
+     * @var string[] List of js files required for the page.
+     */
+    private $js = [];
+    
+    /**
+     * @var string Path to the view associated with the page.
+     */
+    private $view;
+    
+    /**
+     * @var string Title of the page, in the defined language.
+     */
+    private $title;
+    
+    /**
+     * @var string Site name.
+     */
+    private $name;
+    
+    /**
+     * @var string Page description, in the defined language.
+     */
+    private $description;
+    
+    /**
+     * @var string URL to the site favicon.
+     */
+    private $favicon;
+    
+    /**
+     * @var string URL to the page icon or main image.
+     */
+    private $icon;
+    
+    /**
+     * @var string Canonical URL.
+     */
+    private $canonical;
+    
+    /**
+     * @var string Autjhor URL.
+     */
+    private $author;
+    
+    /**
+     * @var integer HTTP status code.
+     */
+    private $code = 0;
+    
+    /**
+     * @var string HTTP status message.
+     */
+    private $message = "";
+    
+    /**
+     * Constructor.
+     */
+    public function __construct(){
+        $this->favicon = URL::IMG["LOGO"] . "logo.svg";
+        $this->icon = URL::IMG["LOGO"] . "logo.svg";
+        $this->name = get_context()->get_user()->get_display_name();
+        $this->author = URL::BASE;
+        $this->add_css("ui.css");
+    }
+
+    /**
+     * Adds a CSS file to the list.
+     *
+     * @param string $css Css file. Can be the absolute URL, or just the filename.
+     */
+    protected function add_css($css){
+        if (strripos($css, URL::CSS) === false) $file = URL::CSS . $css;
+        else $file = $css;
+        array_push($this->css, $file);
+    }
+    
+    /**
+     * Adds a JS file to the list.
+     *
+     * @param string $js JS file. Can be the absolute URL, or just the filename.
+     */
+    protected function add_js($js){
+        if (strripos($js, URL::JS) === false) $file = URL::JS . $js;
+        else $file = $js;
+        array_push($this->js, $file);
+    }
+    
+    /**
+     * Retrieves the CSS file URLs for the page.
+     *
+     * @return string[] CSS files (by absolute URL.)
+     */
+    public function get_css(){return $this->css;}
+    
+    /**
+     * Retrieves the JS file URLs for the page.
+     *
+     * @return string[] JS files (by absolute URL.)
+     */
+    public function get_js(){return $this->js;}
+    
+    /**
+     * Sets the page view.
+     *
+     * @param string $view View file. Can be the relative path, or just the filename.
+     */
+    protected function set_view($view){
+        if (strripos($view, PATH::VIEW) === false) $file = PATH::VIEW . $view;
+        else $file = $view;
+        $this->view = $file;
+    }
+    
+    /**
+     * Retrieves the page view file.
+     *
+     * @return string Path to the view file.
+     */
+    public function get_view(){return $this->view;}
+    
+    /**
+     * Sets the page title.
+     *
+     * The user name" will always be added at the end.
+     *
+     * @param string $title Page title.
+     */
+    protected function set_title($title){
+        $this->title = htmlentities($title, ENT_QUOTES);
+        if ($title != "") $this->title = $this->title . " - ";
+        $this->title = $this->title . get_context()->get_user()->get_display_name();
+    }
+    
+    /**
+     * Retrieves the page title.
+     *
+     * @return string Page title.
+     */
+    public function get_title(){return $this->title;}
+    
+    /**
+     * Retrieves the site name.
+     *
+     * @return string Site name.
+     */
+    public function get_name(){return $this->name;}
+    
+    /**
+     * Sets the page description.
+     *
+     * The user tagline will always be added at the end.
+     *
+     * @param string $description Page description text.
+     */
+    protected function set_description($description){
+        $this->description = htmlentities($description, ENT_QUOTES);
+        if ($description != "") $this->description = $this->description . " - ";
+        $this->description = $this->description . get_context()->get_user()->get_text("TAGLINE");
+    }
+    
+    /**
+     * Retrieves the page description
+     *
+     * @return string Page description.
+     */
+    public function get_description(){return $this->description;}
+    
+    /**
+     * Retrieves the URL to the favicon.
+     *
+     * @return string Favicon URL.
+     */
+    public function get_favicon(){return $this->favicon;}
+    
+    /**
+     * Retrieves the URL to the thumbnail image.
+     *
+     * @return string Thmbnail icon URL.
+     */
+    public function get_icon(){return $this->icon;}
+    
+    /**
+     * Sets the page canonical URL.
+     *
+     * @param string $canonical Canonical URL, absolute or relative.
+     */
+    protected function set_canonical($canonical){
+        if (strripos($canonical, URL::BASE) === false) $file = URL::BASE . $canonical;
+        else $file = $canonical;
+        $this->canonical = $file;
+    }
+    
+    /**
+     * Retrieves the canonical URL to the page.
+     *
+     * @return string Canonical URL.
+     */
+    public function get_canonical(){return $this->canonical;}
+    
+    /**
+     * Retrieves the author URL (site main URL).
+     *
+     * @return string AUthor URL.
+     */
+    public function get_author(){return $this->author;}
+    
+    /**
+     * Sets the HTTP status code for the page to return.
+     *
+     * @param int HTTP status code.
+     */
+    protected function set_code($code){
+        if (is_int($code) && $code >= 200 && $code <= 500) $this->code = $code;
+    }
+    
+    /**
+     * Retrieves the page HTTP status code.
+     *
+     * @return int HTTP status code
+     */
+    public function get_code(){return $this->code;}
+    
+    /**
+     * Sets the HTTP status message for the page to return.
+     *
+     * @param string HTTP status message.
+     */
+    protected function set_message($mesage){$this->message = strval($mesage);}
+    
+    /**
+     * Retrieves the page HTTP status message.
+     *
+     * @return string HTTP status code
+     */
+    public function get_message(){return $this->message;}
+    
+    /**
+     * Generates the content to be inserted in the HTML <head> section.
+     *
+     * It must go between the <head> and </head> tags, and anything can go behind it before closing
+     * the section. It includes page title, description, css files and metadata.
+     *
+     * @return string HTML head content.
+     */
+    public function generate_head(){
+        $head = "
+          <meta content='text/html; charset=utf-8' http-equiv='content-type'/>
+          <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1'/>
+          <title>" . $this->get_title() . "</title>
+          <link rel='shortcut icon' href='" . $this->get_favicon() . "'/>
+          <!-- CSS files -->";
+        foreach ($this->get_css() as $css){
+            $head .= "
+          <link rel='stylesheet' type='text/css' href='$css'/>";
+        }
+        $head .= "
+          <!-- JS files -->";
+        foreach ($this->get_js() as $js){
+            $head .= "
+          <script type='text/javascript' src='$js'></script>";
+        }
+        $head .= "
+          <!-- Meta tags -->
+          <link rel='canonical' href='" . $this->get_canonical() . "'/>
+          <link rel='author' href='" . $this->get_author() . "'/>
+          <link rel='publisher' href='" . $this->get_author() . "'/>
+          <meta name='description' content='" . $this->get_description() . "'/>";
+        foreach (get_context()->get_available_langs() as $lang){
+            if ($lang != get_context()->get_lang()){
+                $url = substr($this->get_canonical(), strlen(URL::BASE));
+                if (substr($url, 0, 3) == get_context()->get_lang() . "/")
+                    $url = URL::BASE . $lang . "/" . substr($url, 2);
+                else $url = URL::BASE . $lang . "/" . $url;
+                $head .= "
+          <link rel='alternate' hreflang='$lang' href='$url' />";
+            }
+        }
+        $head .= "
+          <meta property='og:title' content='" . $this->get_title() . "'/>
+          <meta property='og:url' content='" . $this->get_canonical() . "'/>
+          <meta property='og:description' content='" . $this->get_description() . "'/>
+          <meta property='og:image' content='" . $this->get_icon() . "'/>
+          <meta property='og:site_name' content='" . $this->get_name() . "'/>
+          <meta property='og:type' content='website'/>
+          <meta property='og:locale' content='en'/>
+          <meta name='twitter:card' content='summary'/>
+          <meta name='twitter:title' content='" . $this->get_title() . "'/>
+          <meta name='twitter:description' content='" . $this->get_description() . "'/>
+          <meta name='twitter:image' content='" . $this->get_icon() . "'/>
+          <meta name='twitter:url' content='" . $this->get_canonical() . "'/>
+          <meta name='robots' content='index follow'/>\n";
+        return $head;
+    }
+    
+}

+ 75 - 0
application/page/Profile_Page.php

@@ -0,0 +1,75 @@
+<?php
+
+require_once(PATH::PAGE . "Page.php");
+require_once(PATH::ENTITY . "Cv.php");
+
+/**
+ * Profile page model.
+ */
+class Profile_Page extends Page{
+
+    /**
+    * List of available {@see CV}.
+    */
+    private $cv = [];
+
+    /**
+    * {@see CV} in the current languages.
+    */
+    private $main_cv;
+
+    /**
+    * List {@see CV}s in other languages.
+    */
+    private $other_cv = [];
+
+    /**
+    * Constructor.
+    *
+    * Retrieves the data and initializes the variables.
+    */
+    public function __construct(){
+        $this->view = PATH::VIEW . "profile.php";
+        parent::__construct();
+        $this->set_view("profile.php");
+        $statement = get_context()->get_db()->prepare(
+          "SELECT id FROM cv WHERE user = :user AND visible = 1 ORDER BY lang = :lang DESC"
+        );
+        $statement->bindValue(':user', get_context()->get_user()->get_id(), PDO::PARAM_INT);
+        $statement->bindValue(':lang', get_context()->get_lang(), PDO::PARAM_STR);
+        $statement->execute();
+        while ($r_cv = $statement->fetch(PDO::FETCH_ASSOC)){
+            $c = new Cv($r_cv["id"]);
+            array_push($this->cv, $c);
+            if ($c->get_lang()->get_code() == get_context()->get_lang()) $this->main_cv = $c;
+            array_push($this->other_cv, $c);
+        }
+
+        $this->set_title(Text::get("USER_NAME"));
+        $this->set_description(Text::get("SECTION_ME") . " - " . Text::get("USER_NAME"));
+        $this->set_canonical(URL::PROFILE);
+        $this->add_css("profile.css");
+    }
+
+    /**
+     * Retrieves the list of CVs.
+     *
+     * @return CV[] CV list.
+     */
+    public function get_cvs(){return $this->cv;}
+
+    /**
+     * Retrieves the main CV.
+     *
+     * @return Main CV.
+     */
+    public function get_main_cv(){return $this->main_cv;}
+
+    /**
+     * Retrieves the list of CVs excluding the main one.
+     *
+     * @return CV[] CV list, excluding the main one.
+     */
+    public function get_other_cvs(){return $this->other_cv;}
+}
+?>

+ 47 - 0
application/page/Project_Page.php

@@ -0,0 +1,47 @@
+<?php
+
+require_once(PATH::PAGE . "Page.php");
+require_once(PATH::ENTITY . "Project.php");
+
+
+/**
+    * Project page model.
+    */
+class Project_Page extends Page{
+
+    /**
+    * The selected {@see Project}.
+    */
+    private $project;
+
+    /**
+    * Constructor.
+    *
+    * Retrieves the data and initializes the variables.
+    *
+    * @param string $id Project id or permalink.
+    */
+    public function __construct($id){
+        parent:: __construct();
+        $this->view = PATH::VIEW . "project.php";
+        $this->set_view("project.php");
+        $this->project = new Project($id);
+        $this->set_title($this->project->get_title() . " - " . Text::get("USER_NAME"));
+        $this->set_description($this->project->get_header());
+        $this->add_css("project.css");
+        if (strlen($this->project->get_logo()) > 0){
+            $this->icon = URL::IMG["CONTENT"] . "project/x100/" . $this->project->get_logo();
+        }
+        $this->set_canonical(URL::PROJECTS . $this->project->get_permalink() . "/");
+    }
+
+    /**
+     * Retrieves the loade project.
+     *
+     * @return Project Loaded project.
+     */
+    public function get_project(){
+        return $this->project;
+    }
+}
+?>

+ 45 - 0
application/page/Projects_Page.php

@@ -0,0 +1,45 @@
+<?php
+
+require_once(PATH::PAGE . "Page.php");
+require_once(PATH::ENTITY . "Project.php");
+
+/**
+ * Projects list page model.
+ */
+class Projects_Page extends Page{
+
+    /**
+     * Array with the {@see Project}.
+     */
+    private $projects = [];
+
+    /**
+     * Constructor.
+     *
+     * Retrieves the data and initializes the variables.
+     */
+    public function __construct(){
+        $this->view = PATH::VIEW . "projects.php";
+        parent:: __construct();
+        $this->set_view("projects.php");
+        $statement = get_context()->get_db()->prepare(
+          "SELECT id FROM project WHERE user = :user AND visible = 1 ORDER BY idx"
+        );
+        $statement->bindValue(':user', get_context()->get_user()->get_id(), PDO::PARAM_INT);
+        $statement->execute();
+        while ($r_projects = $statement->fetch(PDO::FETCH_ASSOC))
+            array_push($this->projects, new Project($r_projects["id"]));
+        $this->set_title(Text::get("PROJECT_TITLE"));
+        $this->set_description(Text::get("PROJECT_DESCRIPTION"));
+        $this->set_canonical(URL::PROJECTS);
+        $this->add_css("projects.css");
+    }
+
+    /**
+     * Retrieves the projects to show on the page.
+     *
+     * @return Project[] Project list.
+     */
+    public function get_projects(){return $this->projects;}
+}
+?>

+ 32 - 0
application/page/Robots_Page.php

@@ -0,0 +1,32 @@
+<?php
+
+require_once(PATH::PAGE . 'Page.php');
+
+/**
+ * robots.txt file page model.
+ */
+class Robots_Page extends Page{
+
+    /**
+     * robots.txt contents
+     */
+    private $text = "User-agent: *\nDisallow:";
+
+    /**
+     * Constructor.
+     *
+     * Retrieves the data and populates the text.
+     */
+    public function __construct(){
+        parent::__construct();
+        $this->set_view('robots.php');
+    }
+
+    /**
+     * Retrieves the text for the robots.txt file.
+     *
+     * @return string The text for the txt file.
+     */
+    public function get_text(){return $this->text;}
+
+}

+ 74 - 0
application/page/Sitemap_Page.php

@@ -0,0 +1,74 @@
+<?php
+
+require_once(PATH::PAGE . 'Page.php');
+
+/**
+ * Sitemap.xml file page model.
+ */
+class Sitemap_Page extends Page{
+
+    /**
+     * Sitemap contents
+     */
+    private $text = '';
+
+    /**
+     * Constructor.
+     *
+     * Retrieves the data and populates the text.
+     */
+    public function __construct(){
+        parent::__construct();
+        $this->set_view('sitemap.php');
+        // TODO: Don't use static date.
+        $lastmod = "2023-09-14";
+        $this->text .= "<?xml version='1.0' encoding='UTF-8'?>
+<urlset xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'>
+    <url>
+        <loc>" . URL::BASE . "</loc>
+        <lastmod>" . $lastmod ."</lastmod>
+        <changefreq>monthly</changefreq>
+        <priority>1.0</priority>
+    </url>
+    <url>
+        <loc>" . URL::BASE . "profile/</loc>
+        <lastmod>" . $lastmod ."</lastmod>
+        <changefreq>monthly</changefreq>
+        <priority>0.9</priority>
+    </url>
+    <url>
+        <loc>" . URL::BASE . "projects/</loc>
+        <lastmod>" . $lastmod ."</lastmod>
+        <changefreq>monthly</changefreq>
+        <priority>0.8</priority>
+    </url>";
+        $statement = get_context()->get_db()->prepare(
+          'SELECT permalink FROM project WHERE user = :user AND visible = 1 ORDER BY idx'
+        );
+        $statement->bindValue(':user', get_context()->get_user()->get_id(), PDO::PARAM_INT);
+        $statement->execute();
+        $priority = 0.7;
+        $priority_dec = 0.3 / $statement->rowcount();
+        while ($r = $statement->fetch(PDO::FETCH_ASSOC)){
+            $this->text .= "
+    <url>
+        <loc>" . URL::BASE . "projects/" . $r["permalink"]. "</loc>
+        <lastmod>" . $lastmod ."</lastmod>
+        <changefreq>monthly</changefreq>
+        <priority>" . $priority . "</priority>
+    </url>";
+            $priority -= $priority_dec;
+        }
+        $this->text .= "
+</urlset>";
+
+    }
+
+    /**
+     * Retrieves the text for the sitemap XML file.
+     *
+     * @return string The text for the XML.
+     */
+    public function get_text(){return $this->text;}
+
+}

+ 122 - 0
application/util/Log.php

@@ -0,0 +1,122 @@
+<?php
+/**
+ * Logger file.
+ *
+ * Provides an utility to log messages.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+/**
+ * Logger class.
+ *
+ * Logs different types of messages.
+ *
+ * @category Util
+ */
+final class Log{
+    
+    /**
+     * Reads the log configuration in ../config/config.ini
+     * 
+     * @return mixed[] Log configuration.
+     */
+    private static function read_config(){
+        return $configuration = parse_ini_file(__DIR__ . "/../config/config.ini", true)["log"];
+    }
+    
+    /**
+     * Formats a message for logging, including the current time.
+     * 
+     * @param string $tag Log type tag.
+     * @param string $message Message to log.
+     * @param int $code Log message associated code, optional.
+     * @return string Message, ready to be written.
+     */
+    private static function format($tag, $message, $code = null){
+        $msg = "[" . $tag . "][" . date("c") . "]";
+        if (is_int($code)) $msg .= "[$code]";
+        $msg .= " $message\n";
+        return $msg;
+    }
+    
+    /**
+     * Sends a debug message.
+     * 
+     * Will do nothing unless the log level is set to DEBUG.
+     * 
+     * @param string $message Message to log.
+     * @param int $code Message associated code, optional.
+     */
+    public static function debug($message, $code = null){
+        $config = Log::read_config();
+        if ($config["level"] == "debug"){
+            $msg = Log::format("DEBUG", $message, $code);
+            file_put_contents($config["file"], $msg, FILE_APPEND);
+        }
+    }
+
+    /**
+     * Sends an informative message.
+     *
+     * Will do nothing unless the log level is set to DEBUG or INFO.
+     *
+     * @param string $message Message to log.
+     * @param int $code Message associated code, optional.
+     */
+    public static function info($message, $code = null){
+        $config = Log::read_config();
+        if (in_array($config["level"], ["debug", "info"])){
+            $msg = Log::format("INFO ", $message, $code);
+            file_put_contents($config["file"], $msg, FILE_APPEND);
+        }
+    }
+
+    /**
+     * Sends a warning message.
+     *
+     * Will do nothing unless the log level is set to WARN.
+     *
+     * @param string $message Message to log.
+     * @param int $code Message associated code, optional.
+     */
+    public static function warn($message, $code = null){
+        $config = Log::read_config();
+        if (in_array($config["level"], ["debug", "info", "warn"])){
+            $msg = Log::format("WARN ", $message, $code);
+            file_put_contents($config["file"], $msg, FILE_APPEND);
+        }
+    }
+
+    /**
+     * Sends an error message.
+     *
+     * Will do nothing if the log level is set to FATAL.
+     *
+     * @param string $message Message to log.
+     * @param int $code Message associated code, optional.
+     */
+    public static function error($message, $code = null){
+        $config = Log::read_config();
+        if (in_array($config["level"], ["debug", "info", "warn", "error"])){
+            $msg = Log::format("ERROR", $message, $code);
+            file_put_contents($config["file"], $msg, FILE_APPEND);
+        }
+    }
+
+    /**
+     * Sends a fatal error message.
+     *
+     * Fatal messages are always logged.
+     *
+     * @param string $message Message to log.
+     * @param int $code Message associated code, optional.
+     */
+    public static function fatal($message, $code = null){
+        $config = Log::read_config();
+        $msg = Log::format("FATAL", $message, $code);
+        file_put_contents($config["file"], $msg, FILE_APPEND);
+    }
+}

+ 90 - 0
application/util/Path.php

@@ -0,0 +1,90 @@
+<?php
+/**
+ * Path constants file.
+ *
+ * Provides paths to everything in the code tree.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+define('BASE_PATH', $_SERVER["DOCUMENT_ROOT"] . "/");
+
+/**
+ * In-app directories (filesystem, not url).
+ *
+ * @category Data
+ */
+final class PATH{
+    
+    /**
+     * Path to the application direcory.
+     */
+    const APPLICATION = BASE_PATH . "../application/";
+    
+    /**
+     * Path to the public direcory.
+     */
+    const BASE = BASE_PATH;
+    
+    /**
+     * Path to the controller file.
+     */
+    const CONTROLLER = BASE_PATH . "../application/Controller.php";
+    
+    /**
+     * Path to the entity files.
+     */
+    const ENTITY = BASE_PATH . "../application/entity/";
+    
+    /**
+     * Path to the page files.
+     */
+    const PAGE = BASE_PATH . "../application/page/";
+    
+    /**
+     * Path to the action files.
+     */
+    const ACTION = BASE_PATH . "../application/action/";
+    
+    /**
+     * Path to the view files.
+     */
+    const VIEW = BASE_PATH . "../application/view/";
+    
+    /**
+     * Paths to image resources.
+     */
+    const IMG = array(
+        "BASE" => BASE_PATH . "img/",
+        "UNKNOWN" => BASE_PATH . "img/unknown.png",
+        "ICON" => BASE_PATH . "img/icon/",
+        "LOGO" => BASE_PATH . "img/logo/",
+        "CURRENCY" => BASE_PATH . "img/currency/",
+        "UNIT" => BASE_PATH . "img/unit/",
+        "AREA" => BASE_PATH . "img/area/",
+        "SKILL" => BASE_PATH . "img/skill/",
+        "ESSENCE" => BASE_PATH . "img/essence/",
+        "RUNE" => BASE_PATH . "img/rune/",
+        "GRIND" => BASE_PATH . "img/grind/",
+        "ELEMENT" => BASE_PATH . "img/element/",
+        "SKILL_GUILD" => BASE_PATH . "img/skill_guild/",
+        "DECORATION" => BASE_PATH . "img/decoration/",
+        "SOURCE" => BASE_PATH . "img/source/",
+        "SKILL_LEADER" => BASE_PATH . "img/skill_leader/",
+        "INVENTORY" => BASE_PATH . "img/inventory/",
+        "EFFECT" => BASE_PATH . "img/effect/",
+        "BUILDING" => BASE_PATH . "img/building/",
+    );
+    
+    /**
+     * Path to the helper files.
+     */
+    const HELPER = BASE_PATH . "../application/helper/";
+    
+    /**
+     * Path to the text resources.
+     */
+    const TEXT = BASE_PATH . "../application/resources/text/";
+}

+ 72 - 0
application/util/URL.php

@@ -0,0 +1,72 @@
+<?php
+/**
+ * URL constants file.
+ *
+ * Provides the app URLs.
+ *
+ * @author Iñigo Valentin <i@inigovalentin.com>
+ * @license https://www.gnu.org/licenses/gpl-3.0.en.html GNU General Public License V3
+ * @package IV
+ */
+
+/**
+ * The site base URL.
+ */
+define('BASE_URL', NET::get_protocol() . $_SERVER["HTTP_HOST"] . "/");
+
+
+/**
+ * In-app URLs.
+ *
+ * @category Data
+ */
+final class URL{
+
+    /**
+     * Domain, with protcol (includes final '/').
+     */
+    const BASE = BASE_URL;
+    
+    /**
+     * Profile page.
+     */
+    const PROFILE = BASE_URL . "profile/";
+    
+    /**
+     * Projects page.
+     */
+    const PROJECTS = BASE_URL . "projects/";
+
+    /**
+     * Fonts full url (includes final '/').
+     */
+    const FONTS = BASE_URL . "fonts/";
+
+    /**
+     * CSS full url (includes final '/').
+     */
+    const CSS = BASE_URL . "css/";
+    
+    /**
+     * JS full url (includes final '/').
+     */
+    const JS = BASE_URL . "js/";
+
+    /**
+     * Help full url (includes final '/').
+     */
+    const HELP = BASE_URL . "help/";
+
+    /**
+     * URLs to image resources.
+     */
+    const IMG = array(
+        "BASE" => BASE_URL . "img/",
+        "UNKNOWN" => BASE_URL . "img/unknown.png",
+        "ICON" => BASE_URL . "img/layout/icon/",
+        "CONTROL" => BASE_URL . "img/layout/control/",
+        "LOGO" => BASE_URL . "img/layout/logo/",
+        "LAYOUT" => BASE_URL . "img/layout/",
+        "CONTENT" => BASE_URL . "img/content/",
+    );
+}

+ 45 - 0
application/view/error.php

@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html lang='<?=get_context()->get_lang()?>'>
+    <head>
+        <?=$page->generate_head()?>
+    </head>
+    <body>
+<?php
+        include __DIR__ . "/inc/header.php";
+?>
+        <main>
+            <section>
+                <h3><?=Text::get("ERROR_ERROR")?></h3>
+                <div id='left'>
+                    <p id='code'>
+                        <?=$page->code?>
+                    </p>
+                    <p>
+                        <span><?=Text::get("ERROR_ERROR")?></span>
+                        <span><?=Text::get("ERROR_CODE")?></span>
+                    </p>
+                </div>
+                <div id='right'>
+                    <h4>
+                        <?=$page->error_description?>
+                    </h4>
+                    <?=$page->advice?>
+                    <ul>
+                        <li>
+                            <a href='javascript: location.reload();'><?=$page->solution[0]?></a>
+                        </li>
+                        <li>
+                            <a href='javascript: history.go(-1);'><?=$page->solution[1]?></a>
+                        </li>
+                        <li>
+                            <a href='/'><?=$page->solution[2]?></a>
+                        </li>
+                    </ul>
+                </div>
+            </section>
+        </main>
+<?php
+        include __DIR__ . "/inc/footer.php";
+?>
+    </body>
+</html>

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff