WebCalendar

Running WordPress? There’s now a WordPress-native version — WebCalendar for WordPress — that installs as a plugin instead of a separate PHP application, with full recurring-event support, iCal import/export, and a built-in holiday library. It’s free on WordPress.org, and there’s an overview here on k5n.us. The standalone PHP application on this page is still maintained and is not going away.

About WebCalendar

WebCalendar is a PHP-based calendar application that can be configured as a single-user calendar, a multi-user calendar for groups of users, or as an event calendar viewable by visitors. MySQL/MariaDB, SQLite3, PostgreSQL, Oracle, DB2, Interbase, MS SQL Server, or ODBC is required. The version 1.9.X releases are still a little rough around the edges since these include an overhaul of the UI to use Bootstrap and jQuery and a complete rewrite of the web-based installer.

WebCalendar can be setup in a variety of ways, such as…

  • A schedule management system for a single person
  • A schedule management system for a group of people, allowing one or more assistants to manage the calendar of another user
  • An events schedule that anyone can view, allowing visitors to submit new events
  • A calendar server that can be viewed with iCalendar-compliant calendar applications like Mozilla Sunbird, Apple iCal or GNOME Evolution or RSS-enabled applications like Firefox, Thunderbird, RSSOwl, FeedDemon, or BlogExpress.

Overview of Features

  • Multi-user support
  • 30 supported languages: Basque, Bulgarian, Chinese-Big5, Chinese-GB2312, Czech, Danish, Dutch, English-US, Estonian, Finnish, French, Galician, German, Greek, Holo-Big5, Hungarian, Icelandic, Italian, Japanese, Korean, Norwegian, Polish, Portuguese_BR, Portuguese, Romanian, Russian, Spanish, Swedish, Turkish, Welsh (see current list of translations here)
  • Web-based installer
  • Auto-detect user’s language preference from browser settings
  • View calendars by day, week, month or year
  • View another user’s calendar
  • View one or more users’ calendar via layers on top of your own calendar
  • Add/Edit/Delete users
  • Add/Edit/Delete events
  • Repeating events including support for overriding or deleting (exceptions)
  • Configurable custom event fields
  • User-configurable preferences for colors, 12/24 time format, Sun/Mon week start
  • Checks for scheduling conflicts
  • Email reminders for upcoming events
  • Email notifications for new/updated/deleted events
  • Export events to iCalendar
  • Import from iCalendar/ics format
  • Optional general access (no login required) to allow calendar to be viewed by people without a login (useful for event calendars)
  • Users can make their calendar available publicly to anyone with an iCalendar-compliant calendar program (such as Apple’s iCal, Mozilla Calendar or Sunbird)
  • Publishing of free/busy schedules (part of the iCalendar standard)
  • RSS support that puts a user’s calendar into RSS
  • Subscribe to “remote” calendars (hosted elsewhere on the net) in either iCalendar or hCalendar formats (WebCalendar 1.1+)
  • User authentication: Web-based, HTTP, LDAP or NIS

System Requirements

  • PHP 8 or later
  • PHP support and access to one of the following databases:
    • SQLite
    • MySQL/MariaDB
    • Oracle
    • Postgres
    • IBM DB2
  • Access to cron for Linux/Unix systems (to send out reminders)

Development Cost

The following metrics from Ohloh show how much it would have cost to commercially develop WebCalendar.

  • Codebase Size: 138,588 lines
  • Estimated Effort: 34 person-years
  • Estimated Cost: $1,884,469
  • (As of 11 August 2024)

Donations

If you’d like to help support the costs of developing, maintaining and supporting WebCalendar, please consider donating.

Developer Resources

License

WebCalendar is available under the GNU General Public License, version 2.

For more information on this license:

Documentation

Most Recent Changes

Below are the most recent source code commits to github on the master branch.

  • Merge pull request #739 from craigk5n/fix/wizard-validation-race-728
    by craigk5n on September 17, 2026 at 6:42 pm

    Merge pull request #739 from craigk5n/fix/wizard-validation-race-728 fix: discard stale wizard field-validation replies (#728)

  • fix: discard stale wizard field-validation replies (#728)
    by craigk5n on September 17, 2026 at 6:35 pm

    fix: discard stale wizard field-validation replies (#728) The wizard’s fresh-install Selenium job failed on the Admin User step, reporting “Passwords do not match” against two password fields that both read back correctly, then dying 45s later in the unrelated wait for the Finish step. It reproduced on two consecutive runs of the same commit, so it is not the intermittent send_keys character loss it was taken for. wizard.js revalidates a field on every input event, and each validation is an async fetch whose replies are not guaranteed to arrive in the order they were sent: this.fieldValidation[fieldName] = data.valid; … submitBtn.disabled = !allValid; admin_password carries minlength=8, so typing “admin123” sends eight requests of which the first seven are invalid by design. If any of those early replies lands after the reply for the complete value, it overwrites the good result and leaves the submit button disabled. A click on a disabled button is silently dropped, and the test’s fallback JS .click() is dropped too, so the run stalls with no error. This is not only a test artifact: a real user typing a password quickly can be left looking at a Create button that will not respond. Stamp each validation request per field and ignore any reply that is no longer the newest. That keeps the live feedback while making the last answer always the one for the current value. tests/wizard-validation-race.js drives the real class with the DOM stubbed just enough to construct it, answers the newer request first and the stale one last, and asserts the stale reply loses. Confirmed to fail with the guard reverted. It runs on the 8.4 leg of the PHPUnit matrix; ubuntu-latest already provides node, so no extra setup step is needed. The three web-install tests stop typing these fields character by character, which was generating the racing requests in the first place. set_value() assigns the value and dispatches a single input event, so the wizard is asked once with the final value, and wait_for_validated() waits for the wizard’s own is-valid class on each field before clicking Create. Waiting on the button alone would not do: it starts out enabled and is only disabled once a reply comes back bad, so an early click can beat the first reply. type_value() is gone; set_value() cannot drop characters, so its retry loop no longer has anything to protect against. The admin-step try/except now covers only the “is this step present” check. It previously wrapped the whole block, so any TimeoutException raised while filling the form was swallowed as “no Admin User step” and the run continued into a misleading failure later. Verified: 721 PHPUnit tests, 279 files compile, the new guard red without the fix and green with it, all three test files byte-compile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

  • Merge pull request #738 from craigk5n/fix/vtimezone-duplicate-key
    by craigk5n on September 17, 2026 at 6:19 pm

    Merge pull request #738 from craigk5n/fix/vtimezone-duplicate-key fix: delete webcal_timezones by tzid so ICS import cannot hit a duplicate key

  • ci: pin pymysql to 1.2.0 in the MySQL wizard test
    by craigk5n on September 17, 2026 at 5:52 pm

    ci: pin pymysql to 1.2.0 in the MySQL wizard test The MySQL Selenium job started failing on every PR: > if auth_packet.is_auth_switch_request(): E AttributeError: ‘NoneType’ object has no attribute ‘is_auth_switch_request’ /usr/local/lib/python3.9/site-packages/pymysql/connections.py:1031 FAILED tests/web-install-mysql.py::test_new_installation FAILED tests/web-install-mysql.py::test_upgrade_installation pymysql 1.2.1, 1.2.2 and 1.2.3 were all published on 2026-09-17. The rewritten authentication state machine in 1.2.x keeps dispatching until the server sends a terminal packet, but does not handle auth_packet coming back None against mysql:8.0, so the handshake dies before any query runs. The install was unpinned, so the job picked up 1.2.3 within hours of its release; master last passed this job on 2026-09-14 on 1.2.0. Only MySQL is affected — PostgreSQL uses psycopg2 and SQLite needs no client — which is why the other two wizard jobs stayed green. The WebCalendar side was never involved: the Apache logs show the wizard walking to step=finish with 200s throughout, and it is the pytest harness’s own verification connection that fails. Pin to 1.2.0, the last release this suite passed on, with a comment recording why so it can be lifted once upstream fixes the None path. The pin goes in the compose file because that is where this job actually installs its dependencies; docker/requirements-tests.txt is not tracked in the repo and docker/Dockerfile-pytest, which reads it, is referenced only from a commented-out line in an untracked dev compose file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

  • fix: delete webcal_timezones by tzid so ICS import cannot hit a dupli…
    by craigk5n on September 17, 2026 at 5:30 pm

    fix: delete webcal_timezones by tzid so ICS import cannot hit a duplicate key Importing an ICS file died with: Duplicate entry ‘America/New_York’ for key ‘webcal_timezones.PRIMARY’ INSERT INTO webcal_timezones ( tzid, dtstart, dtend, vtimezone ) … webcal_timezones has PRIMARY KEY ( tzid ) in every backend schema — one cached VTIMEZONE per zone. save_vtimezone() pre-deleted on a narrower key, “WHERE tzid = ? AND dtstart = ?”, so a stored row whose dtstart differed from the incoming one (an earlier import against older tzdata rules) was not matched, survived the delete, and collided with the INSERT. That then took down the whole import rather than just the timezone cache: dbi_execute() defaults to $fatalOnError = true, so the “if ( ! dbi_execute (…) )” guard already in that function was unreachable and the user got a raw SQL error page instead of their events. A second path reached the same error inside a single file. $event is not cleared between VTIMEZONE blocks, and the keys it uses are shared with the VEVENT parser, so the second block inherited tzlocation and dtstart from the first: an America/New_York + Europe/London file wrote London’s definition under the tzid America/New_York. – Delete on tzid alone, matching the primary key. – Run both statements non-fatally. A timezone we cannot cache is not a reason to abort an import; get_vtimezone() already tolerates a miss. – Skip the write when there is no TZID and no X-LIC-LOCATION. A blank tzid is not a key, it is a collision with the next nameless block. This also guards an unconditional $event[‘tzid’] read that warned on that input. – Reset the four VTIMEZONE-scoped keys at BEGIN:VTIMEZONE. Existing rows self-heal: the next import now deletes the stale row and rewrites it, so no migration is needed. The schemas are left alone. Widening the key to (tzid, dtstart) would accumulate stale duplicates and break get_vtimezone()’s range lookup. tests/SaveVtimezoneTest.php stubs dbi_execute to capture the emitted SQL. All six tests were confirmed to fail with the fix reverted (4 failures and 1 error), including the Europe/London cross-contamination. Verified: 721 tests, 279 files compile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Download Metrics

  • Downloads via Github: 21061
  • Downloads via SourceForge: 1417603

Related Links

  • Standards
    • RFC 2445: Internet Calendaring and Scheduling Core Object Specification (iCalendar)
    • CalDAV: Calendaring and Scheduling Extensions to WebDAV (DRAFT) 
      [Note: WebCalendar does not yet support CalDAV.)
  • Calendar client applications – You can use the applications to view events stored in WebCalendar if you enable its publishing settings.
  • iCalendar/ics download sites – These sites contain calendars for holidays, sports teams schedules, music converts, etc. You can import these files into WebCalendar.